001package com.identityworksllc.iiq.common.access; 002 003import com.fasterxml.jackson.annotation.JsonAutoDetect; 004import com.fasterxml.jackson.annotation.JsonCreator; 005import com.fasterxml.jackson.annotation.JsonProperty; 006import com.fasterxml.jackson.databind.annotation.JsonSerialize; 007import com.fasterxml.jackson.databind.ser.std.StdJdkSerializers; 008import com.fasterxml.jackson.databind.ObjectMapper; 009import com.identityworksllc.iiq.common.Mappable; 010import com.identityworksllc.iiq.common.Utilities; 011import com.identityworksllc.iiq.common.vo.LogLevel; 012import com.identityworksllc.iiq.common.vo.StampedMessage; 013import org.apache.commons.logging.Log; 014import org.apache.commons.logging.LogFactory; 015import sailpoint.api.MessageAccumulator; 016import sailpoint.tools.Message; 017import sailpoint.tools.Util; 018 019import java.io.IOException; 020import java.io.StringReader; 021import java.util.ArrayList; 022import java.util.List; 023import java.util.Map; 024import java.util.StringJoiner; 025import java.util.concurrent.atomic.AtomicBoolean; 026import java.util.function.BiConsumer; 027import java.util.function.Consumer; 028 029/** 030 * The output of {@link AccessCheck#accessCheck(AccessCheckInput)}, containing the 031 * results of the access check (allowed or not) and some metadata. 032 * 033 * This object can be safely serialized to JSON. 034 */ 035@JsonAutoDetect 036public class AccessCheckResponse implements Mappable, Consumer<Boolean>, BiConsumer<Boolean, String>, MessageAccumulator { 037 /** 038 * A logger used to record errors 039 */ 040 private static final Log log = LogFactory.getLog(AccessCheckResponse.class); 041 /** 042 * Whether the access was allowed 043 */ 044 @JsonSerialize(using = StdJdkSerializers.AtomicBooleanSerializer.class) 045 private final AtomicBoolean allowed; 046 /** 047 * Any output messages from the access check 048 */ 049 private final List<StampedMessage> messages; 050 /** 051 * The timestamp of the check 052 */ 053 private final long timestamp; 054 055 /** 056 * Basic constructor used by actual users of this class 057 */ 058 public AccessCheckResponse() { 059 this.timestamp = System.currentTimeMillis(); 060 this.allowed = new AtomicBoolean(true); 061 this.messages = new ArrayList<>(); 062 } 063 064 /** 065 * Jackson-specific constructor. Don't use this one unless you're a JSON library. 066 * 067 * @param allowed The value of the allowed flag 068 * @param messages The messages (possibly null) to add to a new empty list 069 * @param timestamp The timestamp from the JSON 070 */ 071 @JsonCreator 072 public AccessCheckResponse(@JsonProperty("allowed") boolean allowed, @JsonProperty("messages") List<StampedMessage> messages, @JsonProperty(value = "timestamp", defaultValue = "0") long timestamp) { 073 this.allowed = new AtomicBoolean(allowed); 074 this.messages = new ArrayList<>(); 075 076 if (messages != null) { 077 this.messages.addAll(messages); 078 } 079 080 if (timestamp == 0) { 081 this.timestamp = System.currentTimeMillis(); 082 } else { 083 this.timestamp = timestamp; 084 } 085 } 086 087 /** 088 * Decodes an AccessCheckResponse from the given String, which should be a JSON 089 * formatted value. 090 * 091 * @param input The input JSON 092 * @return The decoded AccessCheckResponse 093 * @throws IOException if JSON decoding fails 094 */ 095 public static AccessCheckResponse decode(String input) throws IOException { 096 ObjectMapper mapper = new ObjectMapper(); 097 return mapper.readValue(new StringReader(input), AccessCheckResponse.class); 098 } 099 100 /** 101 * Decodes an AccessCheckResponse from the given Map, which may have been generated using 102 * this class's {@link #toMap()}. The Map may contain a long 'timestamp', a set of 'messages', 103 * and an 'allowed' boolean. 104 * @param input The Map input 105 * @return The decoded AccessCheckResponse 106 */ 107 public static AccessCheckResponse decode(Map<String, Object> input) { 108 long timestamp = System.currentTimeMillis(); 109 if (input.get("timestamp") instanceof Long) { 110 timestamp = (Long)input.get("timestamp"); 111 } 112 113 List<StampedMessage> messages = new ArrayList<>(); 114 if (input.get("messages") instanceof List) { 115 for(Object o : Util.asList(input.get("messages"))) { 116 if (o instanceof StampedMessage) { 117 messages.add((StampedMessage)o); 118 } else if (o instanceof Message) { 119 messages.add(new StampedMessage((Message)o)); 120 } else if (o instanceof String) { 121 messages.add(new StampedMessage((String)o)); 122 } else { 123 log.debug("Unrecognized object type in 'messages' List: " + Utilities.safeClassName(o)); 124 } 125 } 126 } 127 128 boolean result = Util.otob(input.get("allowed")); 129 130 return new AccessCheckResponse(result, messages, timestamp); 131 } 132 133 /** 134 * A functional interface used to deny access to this thing, if your code 135 * happens to be called from a strange context. 136 * 137 * @param status True if access is allowed, false otherwise 138 */ 139 @Override 140 public void accept(Boolean status) { 141 if (status != null) { 142 if (status) { 143 this.allowed.set(true); 144 } else { 145 this.allowed.set(false); 146 } 147 } 148 } 149 150 /** 151 * A functional interface used to deny access to this thing, with a message 152 * @param status True if access is allowed, false otherwise 153 * @param message A 'deny' message to be used in the deny case 154 */ 155 @Override 156 public void accept(Boolean status, String message) { 157 if (status != null) { 158 if (status) { 159 this.allowed.set(true); 160 addMessage(message); 161 } else { 162 denyMessage(message); 163 } 164 } 165 } 166 167 /** 168 * Adds a message to the collection 169 * @param message The message to add 170 */ 171 public void addMessage(String message) { 172 this.messages.add(new StampedMessage(message)); 173 } 174 175 /** 176 * Adds a message to the collection 177 * @param message The message to add 178 */ 179 public void addMessage(Message message) { 180 this.messages.add(new StampedMessage(message)); 181 } 182 183 /** 184 * Denies access to the thing, setting the allowed flag to false 185 */ 186 public void deny() { 187 this.allowed.set(false); 188 } 189 190 /** 191 * Denies access to the thing, additionally logging a message indicating the denial reason 192 * @param reason the denial reason 193 */ 194 public void denyMessage(String reason) { 195 this.messages.add(new StampedMessage(LogLevel.WARN, reason)); 196 deny(); 197 if (log.isDebugEnabled()) { 198 log.debug("Access denied: " + reason); 199 } 200 } 201 202 /** 203 * Gets the stored messages 204 * @return The stored messages 205 */ 206 public List<StampedMessage> getMessages() { 207 return messages; 208 } 209 210 /** 211 * Gets the timestamp 212 * @return The timestamp 213 */ 214 public long getTimestamp() { 215 return timestamp; 216 } 217 218 /** 219 * Returns true if the access was allowed 220 * @return True if access was allowed 221 */ 222 public boolean isAllowed() { 223 return this.allowed.get(); 224 } 225 226 /** 227 * Merges this response with another response. If either response indicates that 228 * access is not allowed, then it will be set to false in this object. Also, messages 229 * will be merged. 230 * 231 * @param other The other object to merge 232 */ 233 public void merge(AccessCheckResponse other) { 234 if (!other.allowed.get()) { 235 deny(); 236 } 237 238 messages.addAll(other.messages); 239 } 240 241 @Override 242 public String toString() { 243 return new StringJoiner(", ", AccessCheckResponse.class.getSimpleName() + "[", "]") 244 .add("allowed=" + allowed) 245 .add("messages=" + messages) 246 .add("timestamp=" + timestamp) 247 .toString(); 248 } 249}