فهرست منبع

fix #2469 saml support

Shinsuke Sugaya 5 سال پیش
والد
کامیت
59a80ec2d5

+ 5 - 0
pom.xml

@@ -1305,6 +1305,11 @@
 			<artifactId>okhttp</artifactId>
 			<artifactId>okhttp</artifactId>
 			<version>${okhttp.version}</version>
 			<version>${okhttp.version}</version>
 		</dependency>
 		</dependency>
+		<dependency>
+			<groupId>com.onelogin</groupId>
+			<artifactId>java-saml</artifactId>
+			<version>2.5.0</version>
+		</dependency>
 
 
 		<!-- suggest library -->
 		<!-- suggest library -->
 		<dependency>
 		<dependency>

+ 139 - 0
src/main/java/org/codelibs/fess/app/web/base/login/SamlCredential.java

@@ -0,0 +1,139 @@
+package org.codelibs.fess.app.web.base.login;
+
+import static org.codelibs.core.stream.StreamUtil.split;
+import static org.codelibs.core.stream.StreamUtil.stream;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.codelibs.core.lang.StringUtil;
+import org.codelibs.fess.entity.FessUser;
+import org.codelibs.fess.helper.SystemHelper;
+import org.codelibs.fess.mylasta.direction.FessConfig;
+import org.codelibs.fess.util.ComponentUtil;
+import org.lastaflute.web.login.credential.LoginCredential;
+
+import com.onelogin.saml2.Auth;
+
+public class SamlCredential implements LoginCredential, FessCredential {
+
+    private final Map<String, List<String>> attributes;
+
+    private final String nameId;
+
+    private final String nameIdFormat;
+
+    private final String sessionIndex;
+
+    private final String nameidNameQualifier;
+
+    private final String nameidSPNameQualifier;
+
+    public SamlCredential(final Auth auth) {
+        attributes = auth.getAttributes();
+        nameId = auth.getNameId();
+        nameIdFormat = auth.getNameIdFormat();
+        sessionIndex = auth.getSessionIndex();
+        nameidNameQualifier = auth.getNameIdNameQualifier();
+        nameidSPNameQualifier = auth.getNameIdSPNameQualifier();
+    }
+
+    @Override
+    public String toString() {
+        return "{" + getUserId() + "}";
+    }
+
+    @Override
+    public String getUserId() {
+        return nameId;
+    }
+
+    public SamlUser getUser() {
+        return new SamlUser(getUserId(), getDefaultGroupsAsArray(), getDefaultRolesAsArray());
+    }
+
+    protected String[] getDefaultGroupsAsArray() {
+        final List<String> list = new ArrayList<>();
+        final FessConfig fessConfig = ComponentUtil.getFessConfig();
+        final String key = fessConfig.getSystemProperty("saml.attribute.group.name", "memberOf");
+        if (StringUtil.isNotBlank(key)) {
+            final List<String> nameList = attributes.get(key);
+            if (nameList != null) {
+                list.addAll(nameList);
+            }
+        }
+        final String value = fessConfig.getSystemProperty("saml.default.groups");
+        if (StringUtil.isNotBlank(value)) {
+            split(value, ",").of(stream -> stream.forEach(list::add));
+        }
+        return list.stream().filter(StringUtil::isNotBlank).map(String::trim).toArray(n -> new String[n]);
+    }
+
+    protected String[] getDefaultRolesAsArray() {
+        final List<String> list = new ArrayList<>();
+        final FessConfig fessConfig = ComponentUtil.getFessConfig();
+        final String key = fessConfig.getSystemProperty("saml.attribute.role.name");
+        if (StringUtil.isNotBlank(key)) {
+            final List<String> nameList = attributes.get(key);
+            if (nameList != null) {
+                list.addAll(nameList);
+            }
+        }
+        final String value = fessConfig.getSystemProperty("saml.default.roles");
+        if (StringUtil.isNotBlank(value)) {
+            split(value, ",").of(stream -> stream.forEach(list::add));
+        }
+        return list.stream().filter(StringUtil::isNotBlank).map(String::trim).toArray(n -> new String[n]);
+    }
+
+    public static class SamlUser implements FessUser {
+
+        private static final long serialVersionUID = 1L;
+
+        protected final String name;
+
+        protected String[] groups;
+
+        protected String[] roles;
+
+        protected String[] permissions;
+
+        protected SamlUser(final String name, final String[] groups, final String[] roles) {
+            this.name = name;
+            this.groups = groups;
+            this.roles = roles;
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String[] getRoleNames() {
+            return roles;
+        }
+
+        @Override
+        public String[] getGroupNames() {
+            return groups;
+        }
+
+        @Override
+        public String[] getPermissions() {
+            if (permissions == null) {
+                final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
+                final Set<String> permissionSet = new HashSet<>();
+                permissionSet.add(systemHelper.getSearchRoleByUser(name));
+                stream(groups).of(stream -> stream.forEach(s -> permissionSet.add(systemHelper.getSearchRoleByGroup(s))));
+                stream(roles).of(stream -> stream.forEach(s -> permissionSet.add(systemHelper.getSearchRoleByRole(s))));
+                permissions = permissionSet.toArray(new String[permissionSet.size()]);
+            }
+            return permissions;
+        }
+
+    }
+}

+ 15 - 0
src/main/java/org/codelibs/fess/app/web/sso/SsoAction.java

@@ -21,7 +21,9 @@ import org.codelibs.fess.app.web.RootAction;
 import org.codelibs.fess.app.web.base.FessLoginAction;
 import org.codelibs.fess.app.web.base.FessLoginAction;
 import org.codelibs.fess.app.web.base.login.ActionResponseCredential;
 import org.codelibs.fess.app.web.base.login.ActionResponseCredential;
 import org.codelibs.fess.app.web.login.LoginAction;
 import org.codelibs.fess.app.web.login.LoginAction;
+import org.codelibs.fess.sso.SsoAuthenticator;
 import org.codelibs.fess.sso.SsoManager;
 import org.codelibs.fess.sso.SsoManager;
+import org.codelibs.fess.sso.saml.SamlAuthenticator;
 import org.codelibs.fess.util.ComponentUtil;
 import org.codelibs.fess.util.ComponentUtil;
 import org.dbflute.optional.OptionalThing;
 import org.dbflute.optional.OptionalThing;
 import org.lastaflute.web.Execute;
 import org.lastaflute.web.Execute;
@@ -74,4 +76,17 @@ public class SsoAction extends FessLoginAction {
             return redirect(LoginAction.class);
             return redirect(LoginAction.class);
         }
         }
     }
     }
+
+    @Execute
+    public ActionResponse metadata(final String name) {
+        String key = name + "Authenticator";
+        if (ComponentUtil.hasComponent(key)) {
+            throw responseManager.new400("Unknown request type: " + name);
+        }
+        final SsoAuthenticator authenticator = ComponentUtil.getComponent(key);
+        if (authenticator instanceof SamlAuthenticator) {
+            return ((SamlAuthenticator) authenticator).getMetadataResponse();
+        }
+        throw responseManager.new400("Unsupported request type: " + name);
+    }
 }
 }

+ 209 - 0
src/main/java/org/codelibs/fess/sso/saml/SamlAuthenticator.java

@@ -0,0 +1,209 @@
+package org.codelibs.fess.sso.saml;
+
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.codelibs.core.lang.StringUtil;
+import org.codelibs.core.misc.DynamicProperties;
+import org.codelibs.core.net.UuidUtil;
+import org.codelibs.fess.app.web.base.login.ActionResponseCredential;
+import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver;
+import org.codelibs.fess.app.web.base.login.SamlCredential;
+import org.codelibs.fess.crawler.Constants;
+import org.codelibs.fess.exception.SsoLoginException;
+import org.codelibs.fess.sso.SsoAuthenticator;
+import org.codelibs.fess.util.ComponentUtil;
+import org.dbflute.optional.OptionalEntity;
+import org.lastaflute.web.login.credential.LoginCredential;
+import org.lastaflute.web.response.ActionResponse;
+import org.lastaflute.web.response.HtmlResponse;
+import org.lastaflute.web.response.StreamResponse;
+import org.lastaflute.web.util.LaRequestUtil;
+import org.lastaflute.web.util.LaResponseUtil;
+
+import com.onelogin.saml2.Auth;
+import com.onelogin.saml2.settings.Saml2Settings;
+import com.onelogin.saml2.settings.SettingsBuilder;
+
+public class SamlAuthenticator implements SsoAuthenticator {
+
+    private static final Logger logger = LogManager.getLogger(SamlAuthenticator.class);
+
+    protected static final String SAML_PREFIX = "saml.";
+
+    protected static final String SAML_STATE = "SAML_STATE";
+
+    private Map<String, Object> defaultSettings;
+
+    @PostConstruct
+    public void init() {
+        if (logger.isDebugEnabled()) {
+            logger.debug("Initialize {}", this.getClass().getSimpleName());
+        }
+        ComponentUtil.getSsoManager().register(this);
+
+        defaultSettings = new HashMap<>();
+        defaultSettings.put("onelogin.saml2.strict", "true");
+        defaultSettings.put("onelogin.saml2.debug", "false");
+        defaultSettings.put("onelogin.saml2.sp.entityid", "http://localhost:8080/sso/metadata/saml");
+        defaultSettings.put("onelogin.saml2.sp.assertion_consumer_service.url", "http://localhost:8080/sso/");
+        defaultSettings.put("onelogin.saml2.sp.assertion_consumer_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
+        defaultSettings.put("onelogin.saml2.sp.single_logout_service.url", "http://localhost:8080/sso/logout/saml");
+        defaultSettings.put("onelogin.saml2.sp.single_logout_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
+        defaultSettings.put("onelogin.saml2.sp.nameidformat", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
+        defaultSettings.put("onelogin.saml2.sp.x509cert", "");
+        defaultSettings.put("onelogin.saml2.sp.privatekey", "");
+        defaultSettings.put("onelogin.saml2.idp.single_sign_on_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
+        defaultSettings.put("onelogin.saml2.idp.single_logout_service.response.url", "");
+        defaultSettings.put("onelogin.saml2.idp.single_logout_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
+        defaultSettings.put("onelogin.saml2.security.nameid_encrypted", "false");
+        defaultSettings.put("onelogin.saml2.security.authnrequest_signed", "false");
+        defaultSettings.put("onelogin.saml2.security.logoutrequest_signed", "false");
+        defaultSettings.put("onelogin.saml2.security.logoutresponse_signed", "false");
+        defaultSettings.put("onelogin.saml2.security.want_messages_signed", "false");
+        defaultSettings.put("onelogin.saml2.security.want_assertions_signed", "false");
+        defaultSettings.put("onelogin.saml2.security.sign_metadata", "");
+        defaultSettings.put("onelogin.saml2.security.want_assertions_encrypted", "false");
+        defaultSettings.put("onelogin.saml2.security.want_nameid_encrypted", "false");
+        defaultSettings.put("onelogin.saml2.security.requested_authncontext", "urn:oasis:names:tc:SAML:2.0:ac:classes:Password");
+        defaultSettings.put("onelogin.saml2.security.onelogin.saml2.security.requested_authncontextcomparison", "exact");
+        defaultSettings.put("onelogin.saml2.security.want_xml_validation", "true");
+        defaultSettings.put("onelogin.saml2.security.signature_algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
+        defaultSettings.put("onelogin.saml2.organization.name", "CodeLibs");
+        defaultSettings.put("onelogin.saml2.organization.displayname", "Fess");
+        defaultSettings.put("onelogin.saml2.organization.url", "https://fess.codelibs.org/");
+        defaultSettings.put("onelogin.saml2.organization.lang", "");
+        defaultSettings.put("onelogin.saml2.contacts.technical.given_name", "Technical Guy");
+        defaultSettings.put("onelogin.saml2.contacts.technical.email_address", "technical@example.com");
+        defaultSettings.put("onelogin.saml2.contacts.support.given_name", "Support Guy");
+        defaultSettings.put("onelogin.saml2.contacts.support.email_address", "support@@example.com");
+    }
+
+    protected Saml2Settings getSettings() {
+        final Map<String, Object> params = new HashMap<>(defaultSettings);
+        final DynamicProperties systemProperties = ComponentUtil.getSystemProperties();
+        systemProperties.entrySet().stream().forEach(e -> {
+            final String key = e.getKey().toString();
+            if (!key.startsWith(SAML_PREFIX)) {
+                return;
+            }
+            params.put("onelogin.saml2." + key.substring(SAML_PREFIX.length()), e.getValue());
+        });
+        return new SettingsBuilder().fromValues(params).build();
+    }
+
+    @Override
+    public LoginCredential getLoginCredential() {
+        return LaRequestUtil
+                .getOptionalRequest()
+                .map(request -> {
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("Logging in with SAML Authenticator");
+                    }
+
+                    final HttpServletResponse response = LaResponseUtil.getResponse();
+
+                    final HttpSession session = request.getSession(false);
+                    if (session != null) {
+                        final String sesState = (String) session.getAttribute(SAML_STATE);
+                        if (StringUtil.isNotBlank(sesState)) {
+                            session.removeAttribute(SAML_STATE);
+                            try {
+                                final Auth auth = new Auth(getSettings(), request, response);
+                                auth.processResponse();
+
+                                if (!auth.isAuthenticated()) {
+                                    if (logger.isDebugEnabled()) {
+                                        logger.debug("Authentication is failed.");
+                                    }
+                                    return null;
+                                }
+
+                                final List<String> errors = auth.getErrors();
+                                if (!errors.isEmpty()) {
+                                    logger.warn("{}", errors.stream().collect(Collectors.joining(", ")));
+                                    if (auth.isDebugActive() && StringUtil.isNotBlank(auth.getLastErrorReason())) {
+                                        logger.warn("Authentication Failure: {} - Reason: {}",
+                                                errors.stream().collect(Collectors.joining(", ")), auth.getLastErrorReason());
+                                    } else {
+                                        logger.warn("Authentication Failure: {}", errors.stream().collect(Collectors.joining(", ")));
+                                    }
+                                    return null;
+                                }
+
+                                return new SamlCredential(auth);
+                            } catch (final Exception e) {
+                                logger.warn("Authentication is failed.", e);
+                                return null;
+                            }
+                        }
+                    }
+
+                    try {
+                        final Auth auth = new Auth(getSettings(), request, response);
+                        final String loginUrl = auth.login(null, false, false, true, true);
+                        request.getSession().setAttribute(SAML_STATE, UuidUtil.create());
+                        return new ActionResponseCredential(() -> HtmlResponse.fromRedirectPathAsIs(loginUrl));
+                    } catch (final Exception e) {
+                        throw new SsoLoginException("Invalid SAML redirect URL.", e);
+                    }
+
+                }).orElseGet(() -> null);
+    }
+
+    @Override
+    public void resolveCredential(final LoginCredentialResolver resolver) {
+        resolver.resolve(SamlCredential.class, credential -> OptionalEntity.of(credential.getUser()));
+
+    }
+
+    public ActionResponse getMetadataResponse() {
+        return LaRequestUtil
+                .getOptionalRequest()
+                .map(request -> {
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("Logging in with SAML Authenticator");
+                    }
+                    final HttpServletResponse response = LaResponseUtil.getResponse();
+                    try {
+                        final Auth auth = new Auth(getSettings(), request, response);
+                        final Saml2Settings settings = auth.getSettings();
+                        settings.setSPValidationOnly(true);
+                        final String metadata = settings.getSPMetadata();
+                        final List<String> errors = Saml2Settings.validateMetadata(metadata);
+                        if (errors.isEmpty()) {
+                            return new StreamResponse("metadata").contentType("application/xhtml+xml").stream(out -> {
+                                try (final Writer writer = new OutputStreamWriter(out.stream(), Constants.UTF_8_CHARSET)) {
+                                    writer.write(metadata);
+                                }
+                            });
+                        } else {
+                            return getStreamResponse("metadata", "text/html; charset=UTF-8", errors.stream().map(s -> "<p>" + s + "</p>")
+                                    .collect(Collectors.joining()));
+                        }
+                    } catch (final Exception e) {
+                        logger.warn("Failed to process metadata.", e);
+                        return getStreamResponse("metadata", "text/html; charset=UTF-8", e.getMessage());
+                    }
+                }).orElseGet(() -> getStreamResponse("metadata", "text/html; charset=UTF-8", "Invalid state."));
+    }
+
+    protected StreamResponse getStreamResponse(final String filename, final String contentType, final String content) {
+        return new StreamResponse(filename).contentType(contentType).stream(out -> {
+            try (final Writer writer = new OutputStreamWriter(out.stream(), Constants.UTF_8_CHARSET)) {
+                writer.write(content);
+            }
+        });
+    }
+
+}

+ 9 - 5
src/main/java/org/codelibs/fess/util/ComponentUtil.java

@@ -512,24 +512,28 @@ public final class ComponentUtil {
         }
         }
     }
     }
 
 
+    public static boolean hasComponent(final String componentKey) {
+        return SingletonLaContainerFactory.getContainer().hasComponentDef(componentKey);
+    }
+
     public static boolean hasViewHelper() {
     public static boolean hasViewHelper() {
-        return SingletonLaContainerFactory.getContainer().hasComponentDef(VIEW_HELPER);
+        return hasComponent(VIEW_HELPER);
     }
     }
 
 
     public static boolean hasQueryHelper() {
     public static boolean hasQueryHelper() {
-        return SingletonLaContainerFactory.getContainer().hasComponentDef(QUERY_HELPER);
+        return hasComponent(QUERY_HELPER);
     }
     }
 
 
     public static boolean hasPopularWordHelper() {
     public static boolean hasPopularWordHelper() {
-        return SingletonLaContainerFactory.getContainer().hasComponentDef(POPULAR_WORD_HELPER);
+        return hasComponent(POPULAR_WORD_HELPER);
     }
     }
 
 
     public static boolean hasRelatedQueryHelper() {
     public static boolean hasRelatedQueryHelper() {
-        return SingletonLaContainerFactory.getContainer().hasComponentDef(RELATED_QUERY_HELPER);
+        return hasComponent(RELATED_QUERY_HELPER);
     }
     }
 
 
     public static boolean hasIngestFactory() {
     public static boolean hasIngestFactory() {
-        return SingletonLaContainerFactory.getContainer().hasComponentDef(INGEST_FACTORY);
+        return hasComponent(INGEST_FACTORY);
     }
     }
 
 
     public static boolean available() {
     public static boolean available() {

+ 2 - 0
src/main/resources/fess_sso++.xml

@@ -6,6 +6,8 @@
 	</component>
 	</component>
 	<component name="oicAuthenticator" class="org.codelibs.fess.sso.oic.OpenIdConnectAuthenticator">
 	<component name="oicAuthenticator" class="org.codelibs.fess.sso.oic.OpenIdConnectAuthenticator">
 	</component>
 	</component>
+	<component name="samlAuthenticator" class="org.codelibs.fess.sso.saml.SamlAuthenticator">
+	</component>
 	<component name="spnegoAuthenticator" class="org.codelibs.fess.sso.spnego.SpnegoAuthenticator">
 	<component name="spnegoAuthenticator" class="org.codelibs.fess.sso.spnego.SpnegoAuthenticator">
 	</component>
 	</component>
 </components>
 </components>