Sonar Fixes, Formatting
[aaf/authz.git] / cadi / core / src / main / java / org / onap / aaf / cadi / AbsUserCache.java
1 /**
2  * ============LICENSE_START====================================================
3  * org.onap.aaf
4  * ===========================================================================
5  * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved.
6  * ===========================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END====================================================
19  *
20  */
21
22 package org.onap.aaf.cadi;
23
24
25 import java.io.ByteArrayInputStream;
26 import java.io.IOException;
27 import java.security.Principal;
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Timer;
32 import java.util.TimerTask;
33 import java.util.TreeMap;
34 import java.util.concurrent.ConcurrentHashMap;
35
36 import org.onap.aaf.cadi.Access.Level;
37 import org.onap.aaf.cadi.CachedPrincipal.Resp;
38 import org.onap.aaf.cadi.principal.CachedBasicPrincipal;
39
40 /**
41  * Implement Fast lookup and Cache for Local User Info
42  *
43  * Include ability to add and remove Users
44  *
45  * Also includes a Timer Thread (when necessary) to invoke cleanup on expiring Credentials
46  *
47  * @author Jonathan
48  *
49  */
50 public abstract class AbsUserCache<PERM extends Permission> {
51     // Need an obvious key for when there is no Authentication Cred
52     private static final String NO_CRED = "NoCred";
53     static final int MIN_INTERVAL = 1000*60;    // Min 1 min
54     static final int MAX_INTERVAL = 1000*60*60*4; //  4 hour max
55     private static Timer timer;
56     // Map of userName to User
57     private final Map<String, User<PERM>> userMap;
58     private static final Map<String, Miss> missMap = new TreeMap<>();
59     private final Symm missEncrypt;
60
61     private Clean clean;
62     protected Access access;
63
64     protected AbsUserCache(Access access, long cleanInterval, int highCount, int usageCount) {
65         this.access = access;
66         Symm s;
67         try {
68             byte[] gennedKey = Symm.keygen();
69             s = Symm.obtain(new ByteArrayInputStream(gennedKey));
70         } catch (IOException e) {
71             access.log(e);
72             s = Symm.base64noSplit;
73         }
74         missEncrypt = s;
75
76         userMap = new ConcurrentHashMap<>();
77
78
79         if (cleanInterval>0) {
80             cleanInterval = Math.max(MIN_INTERVAL, cleanInterval);
81             synchronized(AbsUserCache.class) { // Lazy instantiate.. in case there is no cleanup needed
82                 if (timer==null) {
83                     timer = new Timer("CADI Cleanup Timer",true);
84                 }
85
86                 timer.schedule(clean = new Clean(access, cleanInterval, highCount, usageCount), cleanInterval, cleanInterval);
87                 access.log(Access.Level.INIT, "Cleaning Thread initialized with interval of",cleanInterval, "ms and max objects of", highCount);
88             }
89         }
90     }
91
92     @SuppressWarnings("unchecked")
93     public AbsUserCache(AbsUserCache<PERM> cache) {
94         this.access = cache.access;
95         userMap = cache.userMap;
96         missEncrypt = cache.missEncrypt;
97
98         synchronized(AbsUserCache.class) {
99             if (cache.clean!=null && cache.clean.lur==null && this instanceof CachingLur) {
100                 cache.clean.lur=(CachingLur<PERM>)this;
101             }
102         }
103     }
104
105     protected void setLur(CachingLur<PERM> lur) {
106         if (clean!=null)clean.lur = lur;
107
108     }
109
110     protected void addUser(User<PERM> user) {
111         Principal p = user.principal;
112         String key;
113         try {
114             if (p instanceof GetCred) {
115                 key = missKey(p.getName(), ((GetCred)p).getCred());
116             } else {
117                 byte[] cred;
118                 if ((cred=user.getCred())==null) {
119                     key = user.name + NO_CRED;
120                 } else {
121                     key = missKey(user.name,cred);
122                 }
123             }
124         } catch (IOException e) {
125             access.log(e);
126             return;
127         }
128         userMap.put(key, user);
129     }
130
131     // Useful for looking up by WebToken, etc.
132     protected void addUser(String key, User<PERM> user) {
133         userMap.put(key, user);
134     }
135
136     /**
137      * Add miss to missMap.  If Miss exists, or too many tries, returns false.
138      *
139      * otherwise, returns true to allow another attempt.
140      *
141      * @param key
142      * @param bs
143      * @return
144      * @throws IOException
145      */
146     protected synchronized boolean addMiss(String key, byte[] bs) {
147         String mkey;
148         try {
149             mkey = missKey(key,bs);
150         } catch (IOException e) {
151             access.log(e);
152             return false;
153         }
154         Miss miss = missMap.get(mkey);
155         if (miss==null) {
156             missMap.put(mkey, new Miss(bs,clean==null?MIN_INTERVAL:clean.timeInterval,key));
157             return true;
158         }
159         return miss.mayContinue();
160     }
161
162     protected Miss missed(String key, byte[] bs) throws IOException {
163         return missMap.get(missKey(key,bs));
164     }
165
166     protected User<PERM> getUser(Principal principal) {
167         String key;
168         if (principal instanceof GetCred) {
169             GetCred gc = (GetCred)principal;
170             try {
171                 key = missKey(principal.getName(), gc.getCred());
172             } catch (IOException e) {
173                 access.log(e, "Error getting key from Principal");
174                 key = principal.getName();
175             }
176         } else {
177             key = principal.getName()+NO_CRED;
178         }
179         User<PERM> u = userMap.get(key);
180         if (u!=null) {
181             u.incCount();
182         }
183         return u;
184     }
185
186     protected User<PERM> getUser(CachedBasicPrincipal cbp) {
187         return getUser(cbp.getName(), cbp.getCred());
188     }
189
190     protected User<PERM> getUser(String user, byte[] cred) {
191         User<PERM> u;
192         String key=null;
193         try {
194             key =missKey(user,cred);
195         } catch (IOException e) {
196             access.log(e);
197             return null;
198         }
199         u = userMap.get(key);
200         if (u!=null) {
201             if (u.permExpired()) {
202                 userMap.remove(key);
203                 u=null;
204             } else {
205                 u.incCount();
206             }
207         }
208         return u;
209     }
210
211     /**
212      * Removes User from the Cache
213      * @param user
214      */
215     protected void remove(User<PERM> user) {
216         userMap.remove(user.principal.getName());
217     }
218
219     /**
220      * Removes user from the Cache
221      *
222      * @param user
223      */
224     public void remove(String user) {
225         Object o = userMap.remove(user);
226         if (o!=null) {
227             access.log(Level.INFO, user,"removed from Client Cache by Request");
228         }
229     }
230
231     /**
232      * Clear all Users from the Client Cache
233      */
234     public void clearAll() {
235         userMap.clear();
236     }
237
238     public final List<DumpInfo> dumpInfo() {
239         List<DumpInfo> rv = new ArrayList<>();
240         for (User<PERM> user : userMap.values()) {
241             rv.add(new DumpInfo(user));
242         }
243         return rv;
244     }
245
246     /**
247      * The default behavior of a LUR is to not handle something exclusively.
248      */
249     public boolean handlesExclusively(Permission ... pond) {
250         return false;
251     }
252
253     /**
254      * Container calls when cleaning up...
255      *
256      * If overloading in Derived class, be sure to call "super.destroy()"
257      */
258     public void destroy() {
259         if (timer!=null) {
260             timer.purge();
261             timer.cancel();
262         }
263     }
264
265
266
267     // Simple map of Group name to a set of User Names
268     //    private Map<String, Set<String>> groupMap = new HashMap<>();
269
270     /**
271      * Class to hold a small subset of the data, because we don't want to expose actual Permission or User Objects
272      */
273     public final class DumpInfo {
274         public String user;
275         public List<String> perms;
276
277         public DumpInfo(User<PERM> user) {
278             this.user = user.principal.getName();
279             perms = new ArrayList<>(user.perms.keySet());
280         }
281     }
282
283     /**
284      * Clean will examine resources, and remove those that have expired.
285      *
286      * If "highs" have been exceeded, then we'll expire 10% more the next time.  This will adjust after each run
287      * without checking contents more than once, making a good average "high" in the minimum speed.
288      *
289      * @author Jonathan
290      *
291      */
292     private final class Clean extends TimerTask {
293         private final Access access;
294         private CachingLur<PERM> lur;
295
296         // The idea here is to not be too restrictive on a high, but to Expire more items by
297         // shortening the time to expire.  This is done by judiciously incrementing "advance"
298         // when the "highs" are exceeded.  This effectively reduces numbers of cached items quickly.
299         private final int high;
300         private long advance;
301         private final long timeInterval;
302         private final int usageTriggerCount;
303
304         public Clean(Access access, long cleanInterval, int highCount, int usageTriggerCount) {
305             this.access = access;
306             lur = null;
307             high = highCount;
308             timeInterval = cleanInterval;
309             advance = 0;
310             this.usageTriggerCount=usageTriggerCount;
311         }
312         public void run() {
313             int renewed = 0;
314             int count = 0;
315             int total = 0;
316             try {
317                 // look at now.  If we need to expire more by increasing "now" by "advance"
318                 ArrayList<User<PERM>> al = new ArrayList<>(userMap.values().size());
319                 al.addAll(0, userMap.values());
320                 long now = System.currentTimeMillis() + advance;
321                 for (User<PERM> user : al) {
322                     ++total;
323                         if (user.count>usageTriggerCount) {
324                             boolean touched = false, removed=false;
325                             if (user.principal instanceof CachedPrincipal) {
326                                 CachedPrincipal cp = (CachedPrincipal)user.principal;
327                                 if (cp.expires() < now) {
328                                     switch(cp.revalidate(null)) {
329                                         case INACCESSIBLE:
330                                             access.log(Level.AUDIT, "AAF Inaccessible.  Keeping credentials");
331                                             break;
332                                         case REVALIDATED:
333                                             user.resetCount();
334                                             touched = true;
335                                             break;
336                                         default:
337                                             user.resetCount();
338                                             remove(user);
339                                             ++count;
340                                             removed = true;
341                                             break;
342                                     }
343                                 }
344                             }
345
346                             if (!removed && lur!=null && user.permExpires<= now ) {
347                                 if (lur.reload(user).equals(Resp.REVALIDATED)) {
348                                     user.renewPerm();
349                                     access.log(Level.DEBUG, "Reloaded Perms for",user);
350                                     touched = true;
351                                 }
352                             }
353                             user.resetCount();
354                             if (touched) {
355                                 ++renewed;
356                             }
357
358                         } else {
359                             if (user.permExpired()) {
360                                 remove(user);
361                                 ++count;
362                             }
363                         }
364                 }
365
366                 // Clean out Misses
367                 int missTotal = missMap.keySet().size();
368                 int miss = 0;
369                 if (missTotal>0) {
370                     ArrayList<String> keys = new ArrayList<>(missTotal);
371                     keys.addAll(missMap.keySet());
372                     for (String key : keys) {
373                         Miss m = missMap.get(key);
374                         if (m!=null) {
375                             long timeLeft = m.timestamp - System.currentTimeMillis();
376                             if (timeLeft<0) {
377                                 synchronized(missMap) {
378                                     missMap.remove(key);
379                                 }
380                                 access.log(Level.INFO, m.name, " has been removed from Missed Credential Map (" + m.tries + " invalid tries)");
381                                 ++miss;
382                             } else {
383                                 access.log(Level.INFO, m.name, " remains in Missed Credential Map (" + m.tries + " invalid tries) for " + (timeLeft/1000) + " more seconds");
384                             }
385                         }
386                     }
387                 }
388
389                 if (count+renewed+miss>0) {
390                     access.log(Level.INFO, (lur==null?"Cache":lur.getClass().getSimpleName()), "removed",count,
391                         "and renewed",renewed,"expired Permissions out of", total,"and removed", miss, "password misses out of",missTotal);
392                 }
393
394                 // If High (total) is reached during this period, increase the number of expired services removed for next time.
395                 // There's no point doing it again here, as there should have been cleaned items.
396                 if (total>high) {
397                     // advance cleanup by 10%, without getting greater than timeInterval.
398                     advance = Math.min(timeInterval, advance+(timeInterval/10));
399                 } else {
400                     // reduce advance by 10%, without getting lower than 0.
401                     advance = Math.max(0, advance-(timeInterval/10));
402                 }
403             } catch (Exception e) {
404                 access.log(Level.ERROR,e.getMessage());
405             }
406         }
407     }
408
409
410     private String missKey(String name, byte[] bs) throws IOException {
411         return name + Hash.toHex(missEncrypt.encode(bs));
412     }
413
414     protected static class Miss {
415         private static final int MAX_TRIES = 3;
416
417         long timestamp;
418
419         private long timetolive;
420
421         private long tries;
422
423         private final String name;
424
425         public Miss(final byte[] first, final long timeInterval, final String name) {
426             timestamp = System.currentTimeMillis() + timeInterval;
427             this.timetolive = timeInterval;
428             tries = 0L;
429             this.name = name;
430         }
431
432
433         public synchronized boolean mayContinue() {
434             long ts = System.currentTimeMillis();
435             if (ts>timestamp) {
436                 tries = 0;
437                 timestamp = ts + timetolive;
438             } else if (MAX_TRIES <= ++tries) {
439                 return false;
440             }
441             return true;
442         }
443
444     }
445
446     /**
447      * Report on state
448      */
449     public String toString() {
450         return getClass().getSimpleName() +
451                 " Cache:\n  Users Cached: " +
452                 userMap.size() +
453                 "\n  Misses Saved: " +
454                 missMap.size() +
455                 '\n';
456
457     }
458
459     public void clear(Principal p, StringBuilder sb) {
460         sb.append(toString());
461         userMap.clear();
462         missMap.clear();
463         access.log(Level.AUDIT, p.getName(),"has cleared User Cache in",getClass().getSimpleName());
464         sb.append("Now cleared\n");
465     }
466
467 }