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