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}