001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://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: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://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.twofactor;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsUser;
032import org.opencms.main.CmsException;
033import org.opencms.main.CmsLog;
034import org.opencms.security.CmsOrganizationalUnit;
035import org.opencms.util.CmsStringUtil;
036
037import java.util.ArrayList;
038import java.util.List;
039import java.util.Set;
040import java.util.regex.Pattern;
041import java.util.stream.Collectors;
042
043import org.apache.commons.logging.Log;
044
045/**
046 * A policy that determines which users should use two-factor authentication.<p>
047 *
048 * A policy consists of two lists of rules: an include list, and an exclude list.
049 * A user should use two-factor authentification if they match at least one rule in
050 * the include list, and no rule in the exclude list. However, if the include list
051 * does not contain any rules, only the exclude list is checked.
052 */
053public class CmsTwoFactorAuthenticationUserPolicy {
054
055    /**
056     * The rule type.
057     */
058    public enum CheckType {
059        /** Checks if the user belongs to a group. */
060        group,
061
062        /** Checks if the full user name (including OU) matches a pattern. */
063        pattern,
064
065        /** Checks if the user belongs to an organization unit (or its sub-units). */
066        orgunit;
067
068    }
069
070    /**
071     * Represents a single rule configured for a policy.
072     */
073    public static class Rule {
074
075        /** The match type, which determines what the name is compared to. */
076        private CheckType m_type;
077
078        /** The value to match. */
079        private String m_value;
080
081        /** Regex pattern used for matching user names. */
082        private Pattern m_pattern;
083
084        /**
085         * Creates a new rule.
086         *
087         * @param type the rule type
088         * @param value the rule value
089         */
090        public Rule(CheckType type, String value) {
091
092            super();
093            m_type = type;
094            m_value = value;
095            if (type == CheckType.pattern) {
096                m_pattern = Pattern.compile(m_value);
097            }
098        }
099
100        /**
101         * Gets the pattern (only used for check type 'pattern').
102         *
103         * @return pattern
104         */
105        public Pattern getPattern() {
106
107            return m_pattern;
108        }
109
110        /**
111         * Gets the match type, which determines what the name is compared to.
112         *
113         * @return the match type
114         */
115        public CheckType getType() {
116
117            return m_type;
118        }
119
120        /**
121         * Gets the name that is used for comparison.
122         *
123         * @return the name
124         */
125        public String getValue() {
126
127            return m_value;
128        }
129
130    }
131
132    /**
133     * A context object used to keep user-related data around which may be needed by multiple rules, so we only need read it once
134     * (e.g. the list of groups of a user).
135     */
136    protected static class UserCheckContext {
137
138        /** The CMS context. */
139        private CmsObject m_cms;
140
141        /** The cached set of group names (lazily initialized). */
142        private Set<String> m_groupNames;
143
144        /** The user. */
145        private CmsUser m_user;
146
147        /**
148         * Creates a new instance.
149         *
150         * @param cms the CMS context
151         * @param user the user
152         */
153        public UserCheckContext(CmsObject cms, CmsUser user) {
154
155            m_cms = cms;
156            m_user = user;
157
158        }
159
160        /**
161         * Gets the set of names of the groups the user belongs to.
162         *
163         * @return the set of group names
164         * @throws CmsException if initializing the groups fails
165         */
166        public Set<String> getGroupNames() throws CmsException {
167
168            if (m_groupNames == null) {
169                m_groupNames = m_cms.getGroupsOfUser(m_user.getName(), false).stream().map(
170                    group -> group.getName()).collect(Collectors.toSet());
171            }
172            return m_groupNames;
173        }
174
175        /**
176         * Gets the user.
177         *
178         * @return the user
179         */
180        public CmsUser getUser() {
181
182            return m_user;
183        }
184
185    }
186
187    /** Logger for this class. */
188    private static final Log LOG = CmsLog.getLog(CmsTwoFactorAuthenticationUserPolicy.class);
189
190    /** The exclude rules. */
191    private List<Rule> m_excludes = new ArrayList<>();
192
193    /** The include rules. */
194    private List<Rule> m_includes = new ArrayList<>();
195
196    /**
197     * Creates a new policy object.
198     *
199     * @param include the list of include rules
200     * @param exclude the list of exclude rules
201     */
202    public CmsTwoFactorAuthenticationUserPolicy(List<Rule> include, List<Rule> exclude) {
203
204        m_includes = new ArrayList<>(include);
205        m_excludes = new ArrayList<>(exclude);
206    }
207
208    /**
209     * Checks whether the given user should use two-factor-authentication according to this policy.
210     *
211     * @param cms the current CMS context
212     * @param user the user to check
213     * @return true if the user should use two-factor authentication
214     */
215    public boolean shouldUseTwoFactorAuthentication(CmsObject cms, CmsUser user) {
216
217        UserCheckContext context = new UserCheckContext(cms, user);
218        return checkIncluded(context) && !checkExcluded(context);
219    }
220
221    /**
222     * Checks if a user (given in a user check context) matches the given rule entry.
223     *
224     * @param context the user check context
225     * @param entry the entry
226     * @return true if the user matches the check entry
227     */
228    private boolean check(UserCheckContext context, Rule entry) {
229
230        if (entry.getType() == CheckType.orgunit) {
231            String entryOu = normalizeOu(entry.getValue());
232            String ou = context.getUser().getOuFqn();
233            while (ou != null) {
234                if (entryOu.equals(normalizeOu(ou))) {
235                    return true;
236                }
237                ou = CmsOrganizationalUnit.getParentFqn(ou);
238            }
239        } else if (entry.getType() == CheckType.group) {
240            try {
241                return context.getGroupNames().contains(entry.getValue());
242            } catch (Exception e) {
243                LOG.error(e.getLocalizedMessage(), e);
244                return false;
245            }
246        } else if (entry.getType() == CheckType.pattern) {
247            return entry.getPattern().matcher(context.getUser().getName()).matches();
248        }
249        return false;
250    }
251
252    /**
253     * Checks if the user from the given context matches the exclude rules.
254     *
255     * @param context the user check context
256     * @return true if the user matches the exclude rules
257     */
258    private boolean checkExcluded(UserCheckContext context) {
259
260        return m_excludes.stream().anyMatch(exclude -> check(context, exclude));
261    }
262
263    /**
264     * Checks if the user from the given context matches the include rules.
265     *
266     * @param context the user check context
267     * @return true if the user matches the include rules
268     */
269    private boolean checkIncluded(UserCheckContext context) {
270
271        if (m_includes.size() == 0) {
272            return true;
273        }
274        return m_includes.stream().anyMatch(include -> check(context, include));
275    }
276
277    /**
278     * Normalizes the OU name with regard to leading / trailing slashes, for comparison purposes.
279     *
280     * @param name the OU name
281     * @return the normalized OU name
282     */
283    private String normalizeOu(String name) {
284
285        return CmsStringUtil.joinPaths("/", name, "/");
286    }
287
288}