Przeglądaj źródła

fix #998 add AuthenticationManager

Shinsuke Sugaya 8 lat temu
rodzic
commit
66e3a91e5f

+ 5 - 10
src/main/java/org/codelibs/fess/app/service/UserService.java

@@ -61,10 +61,7 @@ public class UserService {
     }
 
     public OptionalEntity<User> getUser(final String id) {
-        return userBhv.selectByPK(id).map(u -> {
-            ComponentUtil.getLdapManager().apply(u);
-            return u;
-        });
+        return userBhv.selectByPK(id).map(u -> ComponentUtil.getAuthenticationManager().load(u));
     }
 
     public OptionalEntity<User> getUserByName(final String username) {
@@ -78,7 +75,7 @@ public class UserService {
             user.setSurname(user.getName());
         }
 
-        ComponentUtil.getLdapManager().insert(user);
+        ComponentUtil.getAuthenticationManager().insert(user);
 
         userBhv.insertOrUpdate(user, op -> {
             op.setRefreshPolicy(Constants.TRUE);
@@ -87,10 +84,8 @@ public class UserService {
     }
 
     public void changePassword(final String username, final String password) {
-        final boolean changed = ComponentUtil.getLdapManager().changePassword(username, password);
-
-        final FessConfig fessConfig = ComponentUtil.getFessConfig();
-        if (!changed || fessConfig.isLdapAdminSyncPassword()) {
+        final boolean changed = ComponentUtil.getAuthenticationManager().changePassword(username, password);
+        if (changed) {
             userBhv.selectEntity(cb -> cb.query().setName_Equal(username)).ifPresent(entity -> {
                 final String encodedPassword = fessLoginAssist.encryptPassword(password);
                 entity.setPassword(encodedPassword);
@@ -103,7 +98,7 @@ public class UserService {
     }
 
     public void delete(final User user) {
-        ComponentUtil.getLdapManager().delete(user);
+        ComponentUtil.getAuthenticationManager().delete(user);
 
         userBhv.delete(user, op -> {
             op.setRefreshPolicy(Constants.TRUE);

+ 57 - 0
src/main/java/org/codelibs/fess/auth/AuthenticationManager.java

@@ -0,0 +1,57 @@
+/*
+ * Copyright 2012-2017 CodeLibs Project and the Others.
+ *
+ * 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 org.codelibs.fess.auth;
+
+import static org.codelibs.core.stream.StreamUtil.stream;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.codelibs.core.stream.StreamUtil.StreamOf;
+import org.codelibs.fess.auth.chain.AuthenticationChain;
+import org.codelibs.fess.es.user.exentity.User;
+
+public class AuthenticationManager {
+
+    protected AuthenticationChain[] chains = new AuthenticationChain[0];
+
+    public void insert(final User user) {
+        chains().of(stream -> stream.forEach(c -> c.update(user)));
+    }
+
+    public boolean changePassword(final String username, final String password) {
+        return chains().get(stream -> stream.allMatch(c -> c.changePassword(username, password)));
+    }
+
+    public void delete(final User user) {
+        chains().of(stream -> stream.forEach(c -> c.delete(user)));
+    }
+
+    public User load(final User user) {
+        User u = user;
+        for (final AuthenticationChain chain : chains) {
+            u = chain.load(u);
+        }
+        return u;
+    }
+
+    public void addChain(final AuthenticationChain chain) {
+        chains = ArrayUtils.addAll(chains, chain);
+    }
+
+    protected StreamOf<AuthenticationChain> chains() {
+        return stream(chains);
+    }
+
+}

+ 30 - 0
src/main/java/org/codelibs/fess/auth/chain/AuthenticationChain.java

@@ -0,0 +1,30 @@
+/*
+ * Copyright 2012-2017 CodeLibs Project and the Others.
+ *
+ * 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 org.codelibs.fess.auth.chain;
+
+import org.codelibs.fess.es.user.exentity.User;
+
+public interface AuthenticationChain {
+
+    void update(User user);
+
+    void delete(User user);
+
+    boolean changePassword(String username, String password);
+
+    User load(User user);
+
+}

+ 299 - 0
src/main/java/org/codelibs/fess/auth/chain/CommandChain.java

@@ -0,0 +1,299 @@
+/*
+ * Copyright 2012-2017 CodeLibs Project and the Others.
+ *
+ * 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 org.codelibs.fess.auth.chain;
+
+import static org.codelibs.core.stream.StreamUtil.stream;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.codelibs.core.lang.StringUtil;
+import org.codelibs.fess.crawler.Constants;
+import org.codelibs.fess.crawler.exception.CrawlerSystemException;
+import org.codelibs.fess.es.user.exentity.User;
+import org.codelibs.fess.exception.CommandExecutionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CommandChain implements AuthenticationChain {
+
+    private static final Logger logger = LoggerFactory.getLogger(CommandChain.class);
+
+    protected File workingDirectory = null;
+
+    protected int maxOutputLine = 1000;
+
+    protected long executionTimeout = 30L * 1000L; // 30sec
+
+    protected String commandOutputEncoding = System.getProperty("file.encoding");
+
+    protected String[] updateCommand;
+
+    protected String[] deleteCommand;
+
+    protected String[] targetUsers;
+
+    @Override
+    public void update(final User user) {
+        final String username = user.getName();
+        final String password = user.getOriginalPassword();
+        changePassword(username, password);
+    }
+
+    @Override
+    public void delete(final User user) {
+        final String username = user.getName();
+        if (isTargetUser(username)) {
+            executeCommand(deleteCommand, username, StringUtil.EMPTY);
+        }
+    }
+
+    @Override
+    public boolean changePassword(final String username, final String password) {
+        if (isTargetUser(username) && StringUtil.isNotBlank(password)) {
+            return executeCommand(updateCommand, username, password) == 0;
+        }
+        return true;
+    }
+
+    @Override
+    public User load(final User user) {
+        return user;
+    }
+
+    protected boolean isTargetUser(final String username) {
+        if (targetUsers == null) {
+            return true;
+        }
+        return stream(targetUsers).get(stream -> stream.anyMatch(s -> s.equals(username)));
+    }
+
+    protected int executeCommand(final String[] commands, final String username, final String password) {
+        if (commands == null || commands.length == 0) {
+            throw new CommandExecutionException("command is empty.");
+        }
+
+        if (logger.isInfoEnabled()) {
+            logger.info("Command: " + String.join(" ", commands));
+        }
+
+        final ProcessBuilder pb = new ProcessBuilder((String[]) stream(commands).get(stream -> stream.map(s -> {
+            if ("$USERNAME".equals(s)) {
+                return username;
+            } else if ("$PASSWORD".equals(s)) {
+                return password;
+            } else {
+                return s;
+            }
+        }).toArray(n -> new String[n])));
+        if (workingDirectory != null) {
+            pb.directory(workingDirectory);
+        }
+        pb.redirectErrorStream(true);
+
+        Process currentProcess = null;
+        MonitorThread mt = null;
+        try {
+            currentProcess = pb.start();
+
+            // monitoring
+            mt = new MonitorThread(currentProcess, executionTimeout);
+            mt.start();
+
+            final InputStreamThread it = new InputStreamThread(currentProcess.getInputStream(), commandOutputEncoding, maxOutputLine);
+            it.start();
+
+            currentProcess.waitFor();
+            it.join(5000);
+
+            if (mt.isTeminated()) {
+                throw new CommandExecutionException("The command execution is timeout: " + String.join(" ", commands));
+            }
+
+            final int exitValue = currentProcess.exitValue();
+
+            if (logger.isInfoEnabled()) {
+                logger.info("Exit Code: " + exitValue + " - Process Output:\n" + it.getOutput());
+            }
+            if (exitValue == 143 && mt.isTeminated()) {
+                throw new CommandExecutionException("The command execution is timeout: " + String.join(" ", commands));
+            }
+            return exitValue;
+        } catch (final CrawlerSystemException e) {
+            throw e;
+        } catch (final InterruptedException e) {
+            if (mt != null && mt.isTeminated()) {
+                throw new CommandExecutionException("The command execution is timeout: " + String.join(" ", commands), e);
+            }
+            throw new CommandExecutionException("Process terminated.", e);
+        } catch (final Exception e) {
+            throw new CommandExecutionException("Process terminated.", e);
+        } finally {
+            if (mt != null) {
+                mt.setFinished(true);
+                try {
+                    mt.interrupt();
+                } catch (final Exception e) {
+                    // ignore
+                }
+            }
+            if (currentProcess != null) {
+                try {
+                    currentProcess.destroy();
+                } catch (final Exception e) {
+                    // ignore
+                }
+            }
+            currentProcess = null;
+
+        }
+    }
+
+    protected static class MonitorThread extends Thread {
+        private final Process process;
+
+        private final long timeout;
+
+        private boolean finished = false;
+
+        private boolean teminated = false;
+
+        public MonitorThread(final Process process, final long timeout) {
+            super();
+            this.process = process;
+            this.timeout = timeout;
+        }
+
+        @Override
+        public void run() {
+            try {
+                Thread.sleep(timeout);
+            } catch (final InterruptedException e) {
+                // ignore
+            }
+
+            if (!finished) {
+                try {
+                    process.destroy();
+                    teminated = true;
+                } catch (final Exception e) {
+                    if (logger.isInfoEnabled()) {
+                        logger.info("Could not kill the subprocess.", e);
+                    }
+                }
+            }
+        }
+
+        /**
+         * @param finished
+         *            The finished to set.
+         */
+        public void setFinished(final boolean finished) {
+            this.finished = finished;
+        }
+
+        /**
+         * @return Returns the teminated.
+         */
+        public boolean isTeminated() {
+            return teminated;
+        }
+    }
+
+    protected static class InputStreamThread extends Thread {
+
+        private BufferedReader br;
+
+        private final List<String> list = new LinkedList<>();
+
+        private final int maxLineBuffer;
+
+        public InputStreamThread(final InputStream is, final String charset, final int maxOutputLineBuffer) {
+            super();
+            try {
+                br = new BufferedReader(new InputStreamReader(is, charset));
+            } catch (final UnsupportedEncodingException e) {
+                br = new BufferedReader(new InputStreamReader(is, Constants.UTF_8_CHARSET));
+            }
+            maxLineBuffer = maxOutputLineBuffer;
+        }
+
+        @Override
+        public void run() {
+            for (;;) {
+                try {
+                    final String line = br.readLine();
+                    if (line == null) {
+                        break;
+                    }
+                    if (logger.isDebugEnabled()) {
+                        logger.debug(line);
+                    }
+                    list.add(line);
+                    if (list.size() > maxLineBuffer) {
+                        list.remove(0);
+                    }
+                } catch (final IOException e) {
+                    throw new CrawlerSystemException(e);
+                }
+            }
+        }
+
+        public String getOutput() {
+            final StringBuilder buf = new StringBuilder(100);
+            for (final String value : list) {
+                buf.append(value).append("\n");
+            }
+            return buf.toString();
+        }
+
+    }
+
+    public void setWorkingDirectory(final File workingDirectory) {
+        this.workingDirectory = workingDirectory;
+    }
+
+    public void setMaxOutputLine(final int maxOutputLine) {
+        this.maxOutputLine = maxOutputLine;
+    }
+
+    public void setExecutionTimeout(final long executionTimeout) {
+        this.executionTimeout = executionTimeout;
+    }
+
+    public void setCommandOutputEncoding(final String commandOutputEncoding) {
+        this.commandOutputEncoding = commandOutputEncoding;
+    }
+
+    public void setUpdateCommand(final String[] updateCommand) {
+        this.updateCommand = updateCommand;
+    }
+
+    public void setDeleteCommand(final String[] deleteCommand) {
+        this.deleteCommand = deleteCommand;
+    }
+
+    public void setTargetUsers(final String[] targetUsers) {
+        this.targetUsers = targetUsers;
+    }
+
+}

+ 45 - 0
src/main/java/org/codelibs/fess/auth/chain/LdapChain.java

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2012-2017 CodeLibs Project and the Others.
+ *
+ * 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 org.codelibs.fess.auth.chain;
+
+import org.codelibs.fess.es.user.exentity.User;
+import org.codelibs.fess.util.ComponentUtil;
+
+public class LdapChain implements AuthenticationChain {
+
+    @Override
+    public void update(final User user) {
+        ComponentUtil.getLdapManager().insert(user);
+    }
+
+    @Override
+    public void delete(final User user) {
+        ComponentUtil.getLdapManager().delete(user);
+    }
+
+    @Override
+    public boolean changePassword(final String username, final String password) {
+        boolean changed = ComponentUtil.getLdapManager().changePassword(username, password);
+        return !changed || ComponentUtil.getFessConfig().isLdapAdminSyncPassword();
+    }
+
+    @Override
+    public User load(final User user) {
+        ComponentUtil.getLdapManager().apply(user);
+        return user;
+    }
+
+}

+ 15 - 0
src/main/java/org/codelibs/fess/exception/CommandExecutionException.java

@@ -0,0 +1,15 @@
+package org.codelibs.fess.exception;
+
+public class CommandExecutionException extends FessSystemException {
+
+    private static final long serialVersionUID = 1L;
+
+    public CommandExecutionException(String message) {
+        super(message);
+    }
+
+    public CommandExecutionException(String message, Throwable e) {
+        super(message, e);
+    }
+
+}

+ 7 - 0
src/main/java/org/codelibs/fess/util/ComponentUtil.java

@@ -23,6 +23,7 @@ import org.apache.lucene.queryparser.classic.QueryParser;
 import org.codelibs.core.crypto.CachedCipher;
 import org.codelibs.core.misc.DynamicProperties;
 import org.codelibs.fess.api.WebApiManagerFactory;
+import org.codelibs.fess.auth.AuthenticationManager;
 import org.codelibs.fess.crawler.client.CrawlerClientFactory;
 import org.codelibs.fess.crawler.entity.EsAccessResult;
 import org.codelibs.fess.crawler.extractor.ExtractorFactory;
@@ -77,6 +78,8 @@ public final class ComponentUtil {
 
     private static final Logger logger = LoggerFactory.getLogger(ComponentUtil.class);
 
+    private static final String AUTHENTICATION_MANAGER = "authenticationManager";
+
     private static final String THUMBNAIL_MANAGER = "thumbnailManager";
 
     private static final String SSO_MANAGER = "ssoManager";
@@ -378,6 +381,10 @@ public final class ComponentUtil {
         return getComponent(THUMBNAIL_MANAGER);
     }
 
+    public static AuthenticationManager getAuthenticationManager() {
+        return getComponent(AUTHENTICATION_MANAGER);
+    }
+
     public static PrimaryCipher getPrimaryCipher() {
         return getComponent(PrimaryCipher.class);
     }

+ 32 - 1
src/main/resources/app.xml

@@ -321,7 +321,6 @@
 						<arg>"labels.facet_filetype_others"</arg>
 						<arg>"filetype:others"</arg>
 					</postConstruct>
-
 				</component>
 			</arg>
 		</postConstruct>
@@ -332,6 +331,38 @@
 	</component>
 	<component name="userInfoHelper" class="org.codelibs.fess.helper.UserInfoHelper">
 	</component>
+	<component name="authenticationManager" class="org.codelibs.fess.auth.AuthenticationManager">
+		<!--
+		<postConstruct name="addChain">
+			<arg>
+				<component class="org.codelibs.fess.auth.chain.CommandChain">
+					<property name="updateCommand">[
+					"/usr/sbin/htpasswd",
+					"-b",
+					"/tmp/test.txt",
+					"$USERNAME",
+					"$PASSWORD"
+					]</property>
+					<property name="deleteCommand">[
+					"/usr/sbin/htpasswd",
+					"-D",
+					"/tmp/test.txt",
+					"$USERNAME"
+					]</property>
+					<property name="targetUsers">[
+					"admin"
+					]</property>
+				</component>
+			</arg>
+		</postConstruct>
+		 -->
+		<postConstruct name="addChain">
+			<arg>
+				<component class="org.codelibs.fess.auth.chain.LdapChain">
+				</component>
+			</arg>
+		</postConstruct>
+	</component>
 	<component name="openSearchHelper" class="org.codelibs.fess.helper.OpenSearchHelper">
 		<property name="osddPath">"/WEB-INF/orig/open-search/osdd.xml"</property>
 		<property name="encoding">"UTF-8"</property>