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}