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