diff --git a/server/services/src/main/java/org/zanata/security/SamlIdentity.java b/server/services/src/main/java/org/zanata/security/SamlIdentity.java deleted file mode 100644 index d385b64342..0000000000 --- a/server/services/src/main/java/org/zanata/security/SamlIdentity.java +++ /dev/null @@ -1,95 +0,0 @@ -/* -R * Copyright 2010, Red Hat, Inc. and individual contributors - * as indicated by the @author tags. See the copyright.txt file in the - * distribution for a full listing of individual contributors. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.zanata.security; - -import java.io.Serializable; -import java.security.Principal; - -import javax.enterprise.context.SessionScoped; -import javax.enterprise.event.Event; -import javax.inject.Inject; - -import org.zanata.events.AlreadyLoggedInEvent; -import org.zanata.util.ServiceLocator; -import org.zanata.util.Synchronized; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -@SessionScoped -@Synchronized -public class SamlIdentity implements Serializable { - - private static final long serialVersionUID = 5341594999046279309L; - - @Inject - private ZanataIdentity identity; - - @SuppressFBWarnings("SE_BAD_FIELD") - @Inject - private Event alreadyLoggedInEventEvent; - private String uniqueNameId; - private String email; - private String name; - - public void authenticate(String uniqueNameId, String username, - String email, String name) { - this.uniqueNameId = uniqueNameId; - this.email = email; - this.name = name; - ZanataIdentity identity = - ServiceLocator.instance().getInstance(ZanataIdentity.class); - if (identity.isLoggedIn()) { - getAlreadyLoggedInEvent().fire(new AlreadyLoggedInEvent()); - return; - } - - identity.getCredentials().setUsername(username); - identity.getCredentials().setPassword(""); - identity.getCredentials().setAuthType(AuthenticationType.SSO); - identity.getCredentials().setInitialized(true); - identity.setPreAuthenticated(true); - } - - private Event getAlreadyLoggedInEvent() { - return alreadyLoggedInEventEvent; - } - - public String getUniqueNameId() { - return uniqueNameId; - } - - public String getEmail() { - return email; - } - - public void login(Principal principal) { - if (identity.isLoggedIn()) { - getAlreadyLoggedInEvent().fire(new AlreadyLoggedInEvent()); - return; - } - - identity.acceptExternallyAuthenticatedPrincipal(principal); - } - - public String getName() { - return name; - } -} diff --git a/server/services/src/main/java/org/zanata/security/SamlIdentity.kt b/server/services/src/main/java/org/zanata/security/SamlIdentity.kt new file mode 100644 index 0000000000..0d1589284f --- /dev/null +++ b/server/services/src/main/java/org/zanata/security/SamlIdentity.kt @@ -0,0 +1,82 @@ +/* +R * Copyright 2010, Red Hat, Inc. and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.zanata.security + +import java.io.Serializable +import java.security.Principal + +import javax.enterprise.context.SessionScoped +import javax.enterprise.event.Event +import javax.inject.Inject + +import org.zanata.events.AlreadyLoggedInEvent +import org.zanata.util.ServiceLocator +import org.zanata.util.Synchronized + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings + +@SessionScoped +@Synchronized +class SamlIdentity : Serializable { + + @Inject + lateinit private var identity: ZanataIdentity + + @SuppressFBWarnings("SE_BAD_FIELD") + @Inject + lateinit private var alreadyLoggedInEvent: Event + lateinit var uniqueNameId: String + lateinit var email: String + lateinit var name: String + + fun authenticate(uniqueNameId: String, username: String?, + email: String?, name: String?) { + this.uniqueNameId = uniqueNameId + this.email = email ?: "" + this.name = name ?: "" + + if (identity.isLoggedIn) { + alreadyLoggedInEvent.fire(AlreadyLoggedInEvent()) + return + } + + with (identity) { + credentials.username = username + credentials.password = "" + credentials.authType = AuthenticationType.SSO + credentials.isInitialized = true + isPreAuthenticated = true + } + } + + fun login(principal: Principal) { + if (identity.isLoggedIn) { + alreadyLoggedInEvent.fire(AlreadyLoggedInEvent()) + return + } + + identity.acceptExternallyAuthenticatedPrincipal(principal) + } + + companion object { + private const val serialVersionUID = 5341594999046279309L + } +} diff --git a/server/services/src/main/java/org/zanata/servlet/SAMLFilter.java b/server/services/src/main/java/org/zanata/servlet/SAMLFilter.java deleted file mode 100644 index 1c39895ccf..0000000000 --- a/server/services/src/main/java/org/zanata/servlet/SAMLFilter.java +++ /dev/null @@ -1,137 +0,0 @@ -package org.zanata.servlet; - -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import javax.inject.Inject; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.annotation.WebFilter; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -import org.picketlink.common.constants.GeneralConstants; -import org.picketlink.identity.federation.bindings.wildfly.sp.SPFormAuthenticationMechanism; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.zanata.security.AuthenticationManager; -import org.zanata.util.UrlUtil; - -import io.undertow.security.idm.Account; - -/** - * @author Patrick Huang - * pahuang@redhat.com - */ -@WebFilter(filterName = "ssoFilter") -public class SAMLFilter implements Filter { - private static final Logger log = LoggerFactory.getLogger(SAMLFilter.class); - @Inject - private AuthenticationManager authenticationManager; - @Inject - private UrlUtil urlUtil; - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, - FilterChain chain) throws IOException, ServletException { - if (request instanceof HttpServletRequest) { - HttpServletRequest r = (HttpServletRequest) request; - Object account = r.getSession().getAttribute( - SPFormAuthenticationMechanism.FORM_ACCOUNT_NOTE); - if (account != null && account instanceof Account) { - Account acc = (Account) account; - Optional>> samlAttributeMap = - getSAMLAttributeMap(r.getSession()); - // These assumes IDP follow standard SAML assertion names - Optional usernameFromSSO = - getValueFromSessionAttribute(samlAttributeMap, "uid"); - Optional emailFromSSO = getValueFromSessionAttribute(samlAttributeMap, "email"); - Optional nameFromSSO = getValueFromSessionAttribute(samlAttributeMap, "cn"); - - if (acc.getRoles().contains("authenticated")) { - String principalName = acc.getPrincipal().getName(); - log.info("SSO login: username: {}, name: {}, uuid: {}", - usernameFromSSO, nameFromSSO, principalName); - authenticationManager.ssoLogin(acc, - usernameFromSSO.orElse(principalName), - emailFromSSO.orElse(""), nameFromSSO.orElse("")); - performRedirection((HttpServletResponse) response); - return; - } - } - } - chain.doFilter(request, response); - } - - private static Optional getValueFromSessionAttribute( - Optional>> samlAttributeMap, String key) { - return samlAttributeMap.flatMap(m -> m.get(key).stream().findFirst()); - } - - @SuppressWarnings("unchecked") - private Optional>> - getSAMLAttributeMap(HttpSession session) { - Object attributeMap = - session.getAttribute(GeneralConstants.SESSION_ATTRIBUTE_MAP); - return attributeMap != null && attributeMap instanceof Map - ? Optional.of((Map>) attributeMap) - : Optional.empty(); - } - - @Override - public void destroy() { - } - - /** - * Performs the redirection based on the results from the authentication - * process. - * This is logic that would normally be in faces-config.xml, but as this is - * a servlet, it cannot take advantage of that. - */ - private void performRedirection(HttpServletResponse resp) throws IOException { - String authRedirectResult = - authenticationManager.getAuthenticationRedirect(); - switch (authRedirectResult) { - case "login": - resp.sendRedirect(urlUtil.signInPage()); - break; - - case "edit": - resp.sendRedirect(urlUtil.createUserPage()); - break; - - case "inactive": - resp.sendRedirect(urlUtil.inactiveAccountPage()); - break; - - case "dashboard": - resp.sendRedirect(urlUtil.dashboardUrl()); - break; - case "redirect": - // sso should not have any continue url. We just send to dashboard - resp.sendRedirect(urlUtil.dashboardUrl()); - break; - - case "home": - resp.sendRedirect(urlUtil.home()); - break; - - default: - throw new RuntimeException( - "Unexpected authentication manager result: " + - authRedirectResult); - } - } -} diff --git a/server/services/src/main/java/org/zanata/servlet/SAMLFilter.kt b/server/services/src/main/java/org/zanata/servlet/SAMLFilter.kt new file mode 100644 index 0000000000..763ece8d51 --- /dev/null +++ b/server/services/src/main/java/org/zanata/servlet/SAMLFilter.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2017, Red Hat, Inc. and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.zanata.servlet + +import io.undertow.security.idm.Account +import org.picketlink.common.constants.GeneralConstants +import org.picketlink.identity.federation.bindings.wildfly.sp.SPFormAuthenticationMechanism +import org.slf4j.LoggerFactory +import org.zanata.security.AuthenticationManager +import org.zanata.util.UrlUtil +import java.io.IOException +import javax.inject.Inject +import javax.servlet.* +import javax.servlet.annotation.WebFilter +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +/** + * @author Patrick Huang + * * [pahuang@redhat.com](mailto:pahuang@redhat.com) + */ +@WebFilter(filterName = "ssoFilter") +class SAMLFilter : Filter { + @Inject + lateinit private var authenticationManager: AuthenticationManager + @Inject + lateinit private var urlUtil: UrlUtil + + @Throws(ServletException::class) + override fun init(filterConfig: FilterConfig) {} + + @Throws(IOException::class, ServletException::class) + override fun doFilter(request: ServletRequest, response: ServletResponse, + chain: FilterChain) { + if (request is HttpServletRequest) { + val account: Account? = request.session.getAttribute( + SPFormAuthenticationMechanism.FORM_ACCOUNT_NOTE) as? Account + if (account != null) { + + if (account.roles.contains("authenticated")) { + val principalName = account.principal.name + + val samlAttributeMap: Map?> = + request.session.getAttribute(GeneralConstants.SESSION_ATTRIBUTE_MAP) as? Map?>? ?: mapOf() + // These assumes IDP follow standard SAML assertion names + val usernameFromSSO= getValueFromSessionAttribute(samlAttributeMap, "uid", { _ -> principalName}) + val emailFromSSO = getValueFromSessionAttribute(samlAttributeMap, "email") + val nameFromSSO = getValueFromSessionAttribute(samlAttributeMap, "cn") + log.info("SSO login: username: {}, name: {}, uuid: {}", + usernameFromSSO, nameFromSSO, principalName) + authenticationManager.ssoLogin(account, + usernameFromSSO, emailFromSSO, nameFromSSO) + performRedirection(response as HttpServletResponse) + return + } + } + } + chain.doFilter(request, response) + } + + + override fun destroy() {} + + /** + * Performs the redirection based on the results from the authentication + * process. + * This is logic that would normally be in faces-config.xml, but as this is + * a servlet filter, it cannot take advantage of that. + */ + @Throws(IOException::class) + private fun performRedirection(resp: HttpServletResponse) { + val authRedirectResult = authenticationManager.authenticationRedirect + when (authRedirectResult) { + "login" -> resp.sendRedirect(urlUtil.signInPage()) + "edit" -> resp.sendRedirect(urlUtil.createUserPage()) + "inactive" -> resp.sendRedirect(urlUtil.inactiveAccountPage()) + "dashboard" -> resp.sendRedirect(urlUtil.dashboardUrl()) + "redirect" -> + // sso should not have any continue url. We just send to dashboard + resp.sendRedirect(urlUtil.dashboardUrl()) + "home" -> resp.sendRedirect(urlUtil.home()) + else -> throw RuntimeException( + "Unexpected authentication manager result: " + authRedirectResult) + } + } + + companion object { + private val log = LoggerFactory.getLogger(SAMLFilter::class.java) + + private fun getValueFromSessionAttribute(map: Map?>, key: String, defaultVal : (Int) -> String = {_ -> ""}) = + map[key]?.elementAtOrElse(0, defaultVal) + } +}