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.tools.Message; 016import sailpoint.tools.Util; 017 018import java.io.IOException; 019import java.io.StringReader; 020import java.util.ArrayList; 021import java.util.List; 022import java.util.Map; 023import java.util.StringJoiner; 024import java.util.concurrent.atomic.AtomicBoolean; 025 026/** 027 * The output of {@link AccessCheck#accessCheck(AccessCheckInput)}. 028 */ 029@JsonAutoDetect 030public class AccessCheckResponse implements Mappable { 031 /** 032 * A logger used to record errors 033 */ 034 private static final Log log = LogFactory.getLog(AccessCheckResponse.class); 035 /** 036 * Whether the access was allowed 037 */ 038 @JsonSerialize(using = StdJdkSerializers.AtomicBooleanSerializer.class) 039 private final AtomicBoolean allowed; 040 /** 041 * Any output messages from the access check 042 */ 043 private final List<StampedMessage> messages; 044 /** 045 * The timestamp of the check 046 */ 047 private final long timestamp; 048 049 /** 050 * Basic constructor used by actual users of this class 051 */ 052 public AccessCheckResponse() { 053 this.timestamp = System.currentTimeMillis(); 054 this.allowed = new AtomicBoolean(true); 055 this.messages = new ArrayList<>(); 056 } 057 058 /** 059 * Jackson-specific constructor. Don't use this one unless you're a JSON library. 060 * 061 * @param allowed The value of the allowed flag 062 * @param messages The messages (possibly null) to add to a new empty list 063 * @param timestamp The timestamp from the JSON 064 */ 065 @JsonCreator 066 public AccessCheckResponse(@JsonProperty("allowed") boolean allowed, @JsonProperty("messages") List<StampedMessage> messages, @JsonProperty(value = "timestamp", defaultValue = "0") long timestamp) { 067 this.allowed = new AtomicBoolean(allowed); 068 this.messages = new ArrayList<>(); 069 070 if (messages != null) { 071 this.messages.addAll(messages); 072 } 073 074 if (timestamp == 0) { 075 this.timestamp = System.currentTimeMillis(); 076 } else { 077 this.timestamp = timestamp; 078 } 079 } 080 081 /** 082 * Decodes an AccessCheckResponse from the given String, which should be a JSON 083 * formatted value. 084 * 085 * @param input The input JSON 086 * @return The decoded AccessCheckResponse 087 * @throws IOException if JSON decoding fails 088 */ 089 public static AccessCheckResponse decode(String input) throws IOException { 090 ObjectMapper mapper = new ObjectMapper(); 091 return mapper.readValue(new StringReader(input), AccessCheckResponse.class); 092 } 093 094 /** 095 * Decodes an AccessCheckResponse from the given Map, which may have been generated using 096 * this class's {@link #toMap()}. The Map may contain a long 'timestamp', a set of 'messages', 097 * and an 'allowed' boolean. 098 * @param input The Map input 099 * @return The decoded AccessCheckResponse 100 */ 101 public static AccessCheckResponse decode(Map<String, Object> input) { 102 long timestamp = System.currentTimeMillis(); 103 if (input.get("timestamp") instanceof Long) { 104 timestamp = (Long)input.get("timestamp"); 105 } 106 107 List<StampedMessage> messages = new ArrayList<>(); 108 if (input.get("messages") instanceof List) { 109 for(Object o : Util.asList(input.get("messages"))) { 110 if (o instanceof StampedMessage) { 111 messages.add((StampedMessage)o); 112 } else if (o instanceof Message) { 113 messages.add(new StampedMessage((Message)o)); 114 } else if (o instanceof String) { 115 messages.add(new StampedMessage((String)o)); 116 } else { 117 log.debug("Unrecognized object type in 'messages' List: " + Utilities.safeClassName(o)); 118 } 119 } 120 } 121 122 boolean result = Util.otob(input.get("allowed")); 123 124 return new AccessCheckResponse(result, messages, timestamp); 125 } 126 127 /** 128 * Adds a message to the collection 129 * @param message The message to add 130 */ 131 public void addMessage(String message) { 132 this.messages.add(new StampedMessage(message)); 133 } 134 135 /** 136 * Adds a message to the collection 137 * @param message The message to add 138 */ 139 public void addMessage(Message message) { 140 this.messages.add(new StampedMessage(message)); 141 } 142 143 /** 144 * Denies access to the thing, setting the allowed flag to false 145 */ 146 public void deny() { 147 this.allowed.set(false); 148 } 149 150 /** 151 * Denies access to the thing, additionally logging a message indicating the denial reason 152 * @param reason the denial reason 153 */ 154 public void denyMessage(String reason) { 155 this.messages.add(new StampedMessage(LogLevel.WARN, reason)); 156 deny(); 157 if (log.isDebugEnabled()) { 158 log.debug("Access denied: " + reason); 159 } 160 } 161 162 /** 163 * Gets the stored messages 164 * @return The stored messages 165 */ 166 public List<StampedMessage> getMessages() { 167 return messages; 168 } 169 170 /** 171 * Gets the timestamp 172 * @return The timestamp 173 */ 174 public long getTimestamp() { 175 return timestamp; 176 } 177 178 /** 179 * Returns true if the access was allowed 180 * @return True if access was allowed 181 */ 182 public boolean isAllowed() { 183 return this.allowed.get(); 184 } 185 186 /** 187 * Merges this response with another response. If either response indicates that 188 * access is not allowed, then it will be set to false in this object. Also, messages 189 * will be merged. 190 * 191 * @param other The other object to merge 192 */ 193 public void merge(AccessCheckResponse other) { 194 if (!other.allowed.get()) { 195 deny(); 196 } 197 198 messages.addAll(other.messages); 199 } 200 201 @Override 202 public String toString() { 203 return new StringJoiner(", ", AccessCheckResponse.class.getSimpleName() + "[", "]") 204 .add("allowed=" + allowed) 205 .add("messages=" + messages) 206 .add("timestamp=" + timestamp) 207 .toString(); 208 } 209}