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}