001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.security;
029
030import org.opencms.configuration.CmsConfigurationException;
031import org.opencms.configuration.CmsParameterConfiguration;
032import org.opencms.crypto.CmsAESCBCTextEncryption;
033import org.opencms.crypto.I_CmsTextEncryption;
034import org.opencms.file.CmsObject;
035import org.opencms.main.CmsLog;
036import org.opencms.main.OpenCms;
037
038import java.io.FileInputStream;
039import java.io.IOException;
040import java.io.InputStream;
041import java.nio.file.FileSystems;
042import java.nio.file.Path;
043import java.nio.file.StandardWatchEventKinds;
044import java.nio.file.WatchEvent;
045import java.nio.file.WatchKey;
046import java.nio.file.WatchService;
047import java.util.List;
048import java.util.Properties;
049import java.util.concurrent.TimeUnit;
050import java.util.concurrent.atomic.AtomicBoolean;
051
052import org.apache.commons.logging.Log;
053
054/**
055 * RFS secret store which just loads secrets from a .properties file, whose path is configured via the 'path' parameter.
056 *
057 * <p>This class already initializes itself in the initConfiguration() method, which means it can return secrets before the initialize()
058 * method (which does nothing) is even called.
059 *
060 * <p>When OpenCms is running, this class will also track modifications in the configured secrets file and reload it.
061 */
062public class CmsRfsSecretStore implements I_CmsSecretStore {
063
064    /**
065     * Tracks modifications of the secrets file.
066     */
067    class WatchThread extends Thread {
068
069        public WatchThread() {
070
071            super("CmsRfsSecretStore.WatchThread");
072            // needs to shut down when OpenCms shuts down
073            setDaemon(true);
074        }
075
076        public void run() {
077
078            try (WatchService watch = FileSystems.getDefault().newWatchService()) {
079                m_path.getParent().register(
080                    watch,
081                    StandardWatchEventKinds.ENTRY_CREATE,
082                    StandardWatchEventKinds.ENTRY_DELETE,
083                    StandardWatchEventKinds.ENTRY_MODIFY);
084                while (true) {
085                    try {
086                        WatchKey watchKey = watch.take();
087                        try {
088                            List<WatchEvent<?>> events = watchKey.pollEvents();
089                            for (WatchEvent<?> event : events) {
090                                if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
091                                    continue;
092                                }
093                                WatchEvent<Path> ev = (WatchEvent<Path>)event;
094                                if (ev.context().getFileName().equals(m_path.getFileName())) {
095                                    m_needsReload.set(true);
096                                }
097                            }
098                        } finally {
099                            if (!watchKey.reset()) {
100                                LOG.error(
101                                    "Watch key for " + m_path.getParent() + " has become invalid, stop tracking it");
102                                return;
103                            }
104                        }
105                    } catch (InterruptedException e) {
106                        LOG.error(e.getLocalizedMessage(), e);
107                    }
108                }
109
110            } catch (IOException | UnsupportedOperationException e) {
111                LOG.error(e.getLocalizedMessage(), e);
112            }
113        }
114    }
115
116    /** The parameter used to configure the path of the properties file. */
117    public static final String PARAM_PATH = "path";
118
119    /** The parameter for the encryption password (use no encryption if not set) .*/
120    public static final String PARAM_PASSWORD = "password";
121
122    /** Logger instance for this class. */
123    private static final Log LOG = CmsLog.getLog(CmsRfsSecretStore.class);
124
125    /** The configuration. */
126    private CmsParameterConfiguration m_config = new CmsParameterConfiguration();
127
128    /** True when the secrets file has changed and not been reloaded. */
129    private AtomicBoolean m_needsReload = new AtomicBoolean(false);
130
131    /** The path of the secrets file. */
132    private Path m_path;
133
134    /** The properties. */
135    private volatile Properties m_properties;
136
137    /** The encryption used to decrypt the property values (may be null). */
138    private I_CmsTextEncryption m_encryption;
139
140    /**
141     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#addConfigurationParameter(java.lang.String, java.lang.String)
142     */
143    @Override
144    public void addConfigurationParameter(String key, String value) {
145
146        m_config.add(key, value);
147    }
148
149    /**
150     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration()
151     */
152    @Override
153    public CmsParameterConfiguration getConfiguration() {
154
155        return m_config;
156    }
157
158    /**
159     * @see org.opencms.security.I_CmsSecretStore#getSecret(java.lang.String)
160     */
161    @Override
162    public String getSecret(String key) {
163
164        return m_properties.getProperty(key);
165    }
166
167    /**
168     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#initConfiguration()
169     */
170    @Override
171    public void initConfiguration() throws CmsConfigurationException {
172
173        m_path = Path.of(m_config.get(PARAM_PATH));
174        m_properties = new Properties();
175        Properties props = new Properties();
176        String password = m_config.get(PARAM_PASSWORD);
177        if (password != null) {
178            m_encryption = new CmsAESCBCTextEncryption(password);
179        }
180        try (InputStream stream = new FileInputStream(m_path.toString())) {
181            props.load(stream);
182            m_properties = decrypt(props);
183        } catch (Exception e) {
184            throw new RuntimeException(e);
185        }
186
187    }
188
189    /**
190     * @see org.opencms.security.I_CmsSecretStore#initialize(org.opencms.file.CmsObject)
191     */
192    @Override
193    public void initialize(CmsObject cmsObject) {
194
195        WatchThread thread = new WatchThread();
196        thread.start();
197        OpenCms.getExecutor().scheduleWithFixedDelay(this::checkReload, 1000, 1000, TimeUnit.MILLISECONDS);
198    }
199
200    /**
201     * Reloads the properties file, if necessary.
202     */
203    private void checkReload() {
204
205        if (m_needsReload.compareAndSet(true, false)) {
206            LOG.info("Reloading secrets file...");
207            Properties props = new Properties();
208            try (InputStream stream = new FileInputStream(m_path.toString())) {
209                props.load(stream);
210                m_properties = decrypt(props);
211            } catch (Exception e) {
212                LOG.error(e.getLocalizedMessage(), e);
213            }
214
215        }
216    }
217
218    /**
219     * Decrypts the properties if necessary.
220     *
221     * @param originalProps the original properties
222     * @return the decrypted properties, or the original ones if no decryption is needed
223     */
224    private Properties decrypt(Properties originalProps) {
225
226        if (m_encryption != null) {
227            Properties result = new Properties();
228            for (String propName : originalProps.stringPropertyNames()) {
229                try {
230                    String encryptedValue = originalProps.getProperty(propName);
231                    String decryptedValue = m_encryption.decrypt(encryptedValue);
232                    result.setProperty(propName, decryptedValue);
233                } catch (Exception e) {
234                    LOG.error("Can't decrypt encrypted property " + propName, e);
235                }
236            }
237            return result;
238        } else {
239            return originalProps;
240        }
241    }
242
243}