001package com.identityworksllc.iiq.common.access; 002 003import com.identityworksllc.iiq.common.Metered; 004import com.identityworksllc.iiq.common.Utilities; 005import com.identityworksllc.iiq.common.auth.DummyAuthContext; 006import com.identityworksllc.iiq.common.cache.CacheEntry; 007import com.identityworksllc.iiq.common.cache.Caches; 008import com.identityworksllc.iiq.common.cache.VersionedCacheEntry; 009import org.apache.commons.logging.Log; 010import org.apache.commons.logging.LogFactory; 011import sailpoint.api.SailPointContext; 012import sailpoint.api.SailPointFactory; 013import sailpoint.object.AuditEvent; 014import sailpoint.object.Configuration; 015import sailpoint.object.Identity; 016import sailpoint.rest.plugin.BasePluginResource; 017import sailpoint.server.Auditor; 018import sailpoint.tools.GeneralException; 019import sailpoint.tools.Util; 020import sailpoint.web.UserContext; 021 022import javax.servlet.http.HttpServletRequest; 023import java.util.Map; 024import java.util.Objects; 025import java.util.concurrent.ConcurrentHashMap; 026 027import static com.identityworksllc.iiq.common.access.DelegatedAccessConstants.AUDIT_DA_CHECK; 028 029/** 030 * Utilities for delegated access checks. The access checks are implemented as follows: 031 * 032 * - A Configuration exists that contains ThingAccessUtils-friendly access controls. 033 * 034 * - Access controls are nested and cumulative. If a user can't read from an Identity, 035 * then they trivially can't read any of its attributes. 036 * 037 * - If a 'global' access control exists, its elements will always be added to the controls. 038 * 039 * - A purpose can be specified as a colon-delimited string, e.g., read:private:ssn. Access 040 * controls will be added from 'read', 'read:private', 'read:private:ssn', if they exist. 041 * More specific entries will override less specific entries. More specific entries can 042 * also specify a special '_remove' entry that will suppress upper-level controls. 043 * 044 * If no access controls exist for a purpose string, the answer is always yes. 045 * 046 * The specific purpose strings are arbitrary and are defined in the various IID plugins. 047 * For example, the UI Enhancer has a slew of them, along with its own mechanism for 048 * pointing the access checks at the DA adapter. 049 */ 050public class DelegatedAccessController { 051 052 /** 053 * The cache of delegated access objects 054 */ 055 private static final ConcurrentHashMap<String, CacheEntry<Map<String, Object>>> cache = new ConcurrentHashMap<>(); 056 057 /** 058 * The IIQ context 059 */ 060 private final SailPointContext context; 061 062 /** 063 * The logger 064 */ 065 private final Log log; 066 067 /** 068 * The requester context, usually a plugin API call 069 */ 070 private final UserContext requesterContext; 071 072 /** 073 * Constructs a new delegated access controller with the given IIQ and user context 074 * @param context The IIQ context 075 * @param requester The subject or requester Identity 076 */ 077 public DelegatedAccessController(SailPointContext context, Identity requester) { 078 this(new DummyAuthContext(context, requester.getName())); 079 } 080 081 /** 082 * Constructor for delegated access 083 * @param requesterContext A plugin resource (likely 'this' in your plugin) 084 */ 085 public DelegatedAccessController(UserContext requesterContext) { 086 this.context = requesterContext.getContext(); 087 this.log = LogFactory.getLog(DelegatedAccessController.class); 088 089 // NOTE: You can use ThingAccessUtils.createFakePluginContext() to create one of these 090 this.requesterContext = Objects.requireNonNull(requesterContext); 091 } 092 093 094 /** 095 * Clear cache method, for use via the UI Toolbox cache rule 096 */ 097 public static void clearCache() { 098 cache.clear(); 099 100 Caches.getConfigurationCache().clear(); 101 } 102 103 /** 104 * Returns true if an explicit control exists for the given purpose token. In 105 * other words, if the token is a:b:c, this method returns true only if a:b:c 106 * exists in the configuration. A subset, such as a:b, will not match. 107 * 108 * @param context The IIQ context 109 * @param purpose The colon-delimited purpose 110 * @return True of an explicit control (i.e., does not match as a substring) exists for the given purpose 111 * @throws GeneralException if anything fails 112 */ 113 public static boolean explicitControlExists(SailPointContext context, String purpose) throws GeneralException { 114 Configuration delegatedAccessConfig = getDelegatedAccessConfig(context); 115 116 return delegatedAccessConfig.containsAttribute(purpose); 117 } 118 119 /** 120 * Gets the cached controls for the given purpose or calculates a new one. 121 * This can be a somewhat expensive operation, so caching is critical. Caches 122 * time out after 60 seconds, or the value of configuration key 123 * `IIQCommon.DelegatedAccessController.CacheTimeoutMillis`. 124 * 125 * @param context The SailPointContext to use the load the object 126 * @param purpose The name of the object to retrieve 127 * @return The object retrieved 128 * @throws IllegalArgumentException if there is a problem loading the object 129 */ 130 private static Map<String, Object> getAssembledControls(SailPointContext context, String purpose) throws IllegalArgumentException { 131 return cache.compute(purpose, (key, value) -> { 132 // This is the existing entry in the cache 133 if (value == null || value.isExpired()) { 134 int timeout = Configuration.getSystemConfig().getInt(DelegatedAccessConstants.CONFIG_DA_CACHE_TIMEOUT); 135 if (timeout < 1) { 136 timeout = 60000; 137 } 138 139 try { 140 return new VersionedCacheEntry<>(new DelegatedAccessAssembler(context).assembleControls(purpose), System.currentTimeMillis() + timeout); 141 } catch (GeneralException e) { 142 throw new IllegalArgumentException(e); 143 } 144 } else { 145 return value; 146 } 147 }).getValue(); 148 } 149 150 /** 151 * Gets the delegated access config object if one exists 152 * @param context The IIQ context 153 * @return The Configuration if one exists 154 * @throws GeneralException if the configuration does not exist 155 */ 156 public static Configuration getDelegatedAccessConfig(SailPointContext context) throws GeneralException { 157 SailPointContext previousContext = SailPointFactory.getCurrentContext(); 158 try { 159 SailPointFactory.setContext(context); 160 161 String configName = Configuration.getSystemConfig().getString(DelegatedAccessConstants.CONFIG_DELEGATED_ACCESS); 162 if (Util.isNullOrEmpty(configName)) { 163 throw new GeneralException("The system configuration must contain a property 'configDelegatedAccess'"); 164 } 165 166 Configuration delegatedAccessConfig = Caches.getConfiguration(configName); 167 if (delegatedAccessConfig == null) { 168 throw new GeneralException("The delegated access configuration '" + configName + "' does not exist"); 169 } 170 return delegatedAccessConfig; 171 } finally { 172 SailPointFactory.setContext(previousContext); 173 } 174 } 175 176 /** 177 * Gets the remote IP address of the user from the given HttpServletRequest. This can 178 * be used in a situation where there is no FacesContext, like in a web service call. 179 * 180 * @param request The request to grab the IP from 181 * @return The remote IP of the user 182 */ 183 public static String getRemoteIp(HttpServletRequest request) { 184 String remoteAddr = null; 185 if (request != null) { 186 remoteAddr = request.getHeader("X-FORWARDED-FOR"); 187 if (remoteAddr == null || remoteAddr.isEmpty()) { 188 remoteAddr = request.getRemoteAddr(); 189 } 190 } 191 return remoteAddr; 192 } 193 194 /** 195 * Returns true if, according to the configuration, the logged in user can do the given 196 * action (purpose) against the target user. If the concept is generic, like "can load 197 * plugin page", you can pass null as the target Identity. 198 * 199 * The attempt will not be audited. 200 * 201 * @param target The target user 202 * @param purpose The purpose for which we are checking access (e.g., read, edit, etc) 203 * @return true if access is allowed 204 * @throws GeneralException on any check failures 205 */ 206 public boolean canSeeIdentity(Identity target, String purpose) throws GeneralException { 207 return canSeeIdentity(target, purpose, false); 208 } 209 210 /** 211 * Returns true if, according to the configuration, the logged in user can do the given 212 * action (purpose) against the target user. If the concept is generic, like "can load 213 * plugin page", you can pass null as the target Identity. 214 * 215 * @param target The target user 216 * @param purpose The purpose for which we are checking access (e.g., read, edit, etc) 217 * @param audit True if we should audit this access attempt 218 * @return true if access is allowed 219 * @throws GeneralException on any check failures 220 */ 221 public boolean canSeeIdentity(Identity target, String purpose, boolean audit) throws GeneralException { 222 if (log.isDebugEnabled()) { 223 log.debug("START: Access check for " + target + ", purpose = " + purpose); 224 } 225 226 boolean result = Metered.meter("DelegatedAccessController.canSeeIdentity.check." + purpose, () -> canSeeIdentityImpl(target, purpose)); 227 228 if (log.isDebugEnabled()) { 229 log.debug("FINISH: Access check for " + target + ", purpose = " + purpose + ", allowed = " + result); 230 } 231 232 if (audit) { 233 Metered.meter("DelegatedAccessController.canSeeIdentity.audit", () -> { 234 AuditEvent auditEvent = new AuditEvent(); 235 auditEvent.setSource(requesterContext.getLoggedInUser().getId()); 236 auditEvent.setTarget(target.getId()); 237 auditEvent.setAction(AUDIT_DA_CHECK); 238 auditEvent.setServerHost(Util.getHostName()); 239 if (requesterContext instanceof BasePluginResource) { 240 auditEvent.setClientHost(getRemoteIp(((BasePluginResource) requesterContext).getRequest())); 241 } 242 auditEvent.setString1(purpose); 243 auditEvent.setString2(String.valueOf(result)); 244 245 Utilities.withPrivateContext((context) -> { 246 Auditor.log(auditEvent); 247 }); 248 }); 249 } 250 251 return result; 252 } 253 254 /** 255 * The internal implementation of canSeeIdentity so that it can be easily metered 256 * @param target The target identity 257 * @param purpose The purpose for which we are checking access 258 * @return True if access is allowed 259 * @throws GeneralException on any check failure 260 */ 261 private boolean canSeeIdentityImpl(Identity target, String purpose) throws GeneralException { 262 Map<String, Object> controls = getAssembledControls(context, purpose); 263 264 boolean result; 265 266 if (controls.isEmpty()) { 267 // TODO do we want to default to least-privilege and add privileges? 268 result = true; 269 } else { 270 AccessCheckInput input = new AccessCheckInput(); 271 input.setTarget(target); 272 input.setThingName(purpose); 273 input.setConfiguration(controls); 274 input.setUserContext(this.requesterContext); 275 276 AccessCheckResponse response = AccessCheck.accessCheck(input); 277 result = response.isAllowed(); 278 279 if (log.isDebugEnabled()) { 280 log.debug("Access check response: " + response); 281 } 282 } 283 284 return result; 285 } 286 287}