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