diff --git a/src/main/java/org/codelibs/fess/app/service/UserService.java b/src/main/java/org/codelibs/fess/app/service/UserService.java index 8b6e1cd48..8c2cc26ba 100644 --- a/src/main/java/org/codelibs/fess/app/service/UserService.java +++ b/src/main/java/org/codelibs/fess/app/service/UserService.java @@ -61,10 +61,7 @@ public class UserService { } public OptionalEntity 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 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); diff --git a/src/main/java/org/codelibs/fess/auth/AuthenticationManager.java b/src/main/java/org/codelibs/fess/auth/AuthenticationManager.java new file mode 100644 index 000000000..f44c130a7 --- /dev/null +++ b/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 chains() { + return stream(chains); + } + +} diff --git a/src/main/java/org/codelibs/fess/auth/chain/AuthenticationChain.java b/src/main/java/org/codelibs/fess/auth/chain/AuthenticationChain.java new file mode 100644 index 000000000..b5d9a3e70 --- /dev/null +++ b/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); + +} diff --git a/src/main/java/org/codelibs/fess/auth/chain/CommandChain.java b/src/main/java/org/codelibs/fess/auth/chain/CommandChain.java new file mode 100644 index 000000000..b267f0be3 --- /dev/null +++ b/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 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; + } + +} diff --git a/src/main/java/org/codelibs/fess/auth/chain/LdapChain.java b/src/main/java/org/codelibs/fess/auth/chain/LdapChain.java new file mode 100644 index 000000000..081d5bf60 --- /dev/null +++ b/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; + } + +} diff --git a/src/main/java/org/codelibs/fess/exception/CommandExecutionException.java b/src/main/java/org/codelibs/fess/exception/CommandExecutionException.java new file mode 100644 index 000000000..cab0897c6 --- /dev/null +++ b/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); + } + +} diff --git a/src/main/java/org/codelibs/fess/util/ComponentUtil.java b/src/main/java/org/codelibs/fess/util/ComponentUtil.java index 8eb08b711..43e1e9e59 100644 --- a/src/main/java/org/codelibs/fess/util/ComponentUtil.java +++ b/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); } diff --git a/src/main/resources/app.xml b/src/main/resources/app.xml index d6b69177e..aee234c56 100644 --- a/src/main/resources/app.xml +++ b/src/main/resources/app.xml @@ -321,7 +321,6 @@ "labels.facet_filetype_others" "filetype:others" - @@ -332,6 +331,38 @@ + + + + + + + + + "/WEB-INF/orig/open-search/osdd.xml" "UTF-8"