2 * ============LICENSE_START====================================================
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
11 * http://www.apache.org/licenses/LICENSE-2.0
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====================================================
22 package org.onap.aaf.cadi;
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;
31 import java.util.Timer;
32 import java.util.TimerTask;
33 import java.util.TreeMap;
34 import java.util.concurrent.ConcurrentHashMap;
36 import org.onap.aaf.cadi.Access.Level;
37 import org.onap.aaf.cadi.CachedPrincipal.Resp;
38 import org.onap.aaf.cadi.principal.CachedBasicPrincipal;
41 * Implement Fast lookup and Cache for Local User Info
43 * Include ability to add and remove Users
45 * Also includes a Timer Thread (when necessary) to invoke cleanup on expiring Credentials
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;
62 protected Access access;
64 protected AbsUserCache(Access access, long cleanInterval, int highCount, int usageCount) {
68 byte[] gennedKey = Symm.keygen();
69 s = Symm.obtain(new ByteArrayInputStream(gennedKey));
70 } catch (IOException e) {
72 s = Symm.base64noSplit;
76 userMap = new ConcurrentHashMap<>();
80 cleanInterval = Math.max(MIN_INTERVAL, cleanInterval);
81 synchronized(AbsUserCache.class) { // Lazy instantiate.. in case there is no cleanup needed
83 timer = new Timer("CADI Cleanup Timer",true);
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);
92 @SuppressWarnings("unchecked")
93 public AbsUserCache(AbsUserCache<PERM> cache) {
94 this.access = cache.access;
95 userMap = cache.userMap;
96 missEncrypt = cache.missEncrypt;
98 synchronized(AbsUserCache.class) {
99 if(cache.clean!=null && cache.clean.lur==null && this instanceof CachingLur) {
100 cache.clean.lur=(CachingLur<PERM>)this;
105 protected void setLur(CachingLur<PERM> lur) {
106 if(clean!=null)clean.lur = lur;
110 protected void addUser(User<PERM> user) {
111 Principal p = user.principal;
114 if(p instanceof GetCred) {
115 key = missKey(p.getName(), ((GetCred)p).getCred());
118 if((cred=user.getCred())==null) {
119 key = user.name + NO_CRED;
121 key = missKey(user.name,cred);
124 } catch (IOException e) {
128 userMap.put(key, user);
131 // Useful for looking up by WebToken, etc.
132 protected void addUser(String key, User<PERM> user) {
133 userMap.put(key, user);
137 * Add miss to missMap. If Miss exists, or too many tries, returns false.
139 * otherwise, returns true to allow another attempt.
144 * @throws IOException
146 protected synchronized boolean addMiss(String key, byte[] bs) {
149 mkey = missKey(key,bs);
150 } catch (IOException e) {
154 Miss miss = missMap.get(mkey);
156 missMap.put(mkey, new Miss(bs,clean==null?MIN_INTERVAL:clean.timeInterval,key));
159 return miss.mayContinue();
162 protected Miss missed(String key, byte[] bs) throws IOException {
163 return missMap.get(missKey(key,bs));
166 protected User<PERM> getUser(Principal principal) {
168 if(principal instanceof GetCred) {
169 GetCred gc = (GetCred)principal;
171 key = missKey(principal.getName(), gc.getCred());
172 } catch (IOException e) {
173 access.log(e, "Error getting key from Principal");
174 key = principal.getName();
177 key = principal.getName()+NO_CRED;
179 User<PERM> u = userMap.get(key);
186 protected User<PERM> getUser(CachedBasicPrincipal cbp) {
187 return getUser(cbp.getName(), cbp.getCred());
190 protected User<PERM> getUser(String user, byte[] cred) {
194 key =missKey(user,cred);
195 } catch (IOException e) {
199 u = userMap.get(key);
201 if(u.permExpired()) {
212 * Removes User from the Cache
215 protected void remove(User<PERM> user) {
216 userMap.remove(user.principal.getName());
220 * Removes user from the Cache
224 public void remove(String user) {
225 Object o = userMap.remove(user);
227 access.log(Level.INFO, user,"removed from Client Cache by Request");
232 * Clear all Users from the Client Cache
234 public void clearAll() {
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));
247 * The default behavior of a LUR is to not handle something exclusively.
249 public boolean handlesExclusively(Permission ... pond) {
254 * Container calls when cleaning up...
256 * If overloading in Derived class, be sure to call "super.destroy()"
258 public void destroy() {
267 // Simple map of Group name to a set of User Names
268 // private Map<String, Set<String>> groupMap = new HashMap<>();
271 * Class to hold a small subset of the data, because we don't want to expose actual Permission or User Objects
273 public final class DumpInfo {
275 public List<String> perms;
277 public DumpInfo(User<PERM> user) {
278 this.user = user.principal.getName();
279 perms = new ArrayList<>(user.perms.keySet());
284 * Clean will examine resources, and remove those that have expired.
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.
292 private final class Clean extends TimerTask {
293 private final Access access;
294 private CachingLur<PERM> lur;
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;
304 public Clean(Access access, long cleanInterval, int highCount, int usageTriggerCount) {
305 this.access = access;
308 timeInterval = cleanInterval;
310 this.usageTriggerCount=usageTriggerCount;
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) {
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)) {
330 access.log(Level.AUDIT, "AAF Inaccessible. Keeping credentials");
346 if(!removed && lur!=null && user.permExpires<= now ) {
347 if(lur.reload(user).equals(Resp.REVALIDATED)) {
349 access.log(Level.DEBUG, "Reloaded Perms for",user);
359 if(user.permExpired()) {
367 int missTotal = missMap.keySet().size();
370 ArrayList<String> keys = new ArrayList<>(missTotal);
371 keys.addAll(missMap.keySet());
372 for(String key : keys) {
373 Miss m = missMap.get(key);
375 long timeLeft = m.timestamp - System.currentTimeMillis();
377 synchronized(missMap) {
380 access.log(Level.INFO, m.name, " has been removed from Missed Credential Map (" + m.tries + " invalid tries)");
383 access.log(Level.INFO, m.name, " remains in Missed Credential Map (" + m.tries + " invalid tries) for " + (timeLeft/1000) + " more seconds");
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);
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.
397 // advance cleanup by 10%, without getting greater than timeInterval.
398 advance = Math.min(timeInterval, advance+(timeInterval/10));
400 // reduce advance by 10%, without getting lower than 0.
401 advance = Math.max(0, advance-(timeInterval/10));
403 } catch (Exception e) {
404 access.log(Level.ERROR,e.getMessage());
410 private String missKey(String name, byte[] bs) throws IOException {
411 return name + Hash.toHex(missEncrypt.encode(bs));
414 protected static class Miss {
415 private static final int MAX_TRIES = 3;
419 private long timetolive;
423 private final String name;
425 public Miss(final byte[] first, final long timeInterval, final String name) {
426 timestamp = System.currentTimeMillis() + timeInterval;
427 this.timetolive = timeInterval;
433 public synchronized boolean mayContinue() {
434 long ts = System.currentTimeMillis();
437 timestamp = ts + timetolive;
438 } else if(MAX_TRIES <= ++tries) {
449 public String toString() {
450 return getClass().getSimpleName() +
451 " Cache:\n Users Cached: " +
453 "\n Misses Saved: " +
459 public void clear(Principal p, StringBuilder sb) {
460 sb.append(toString());
463 access.log(Level.AUDIT, p.getName(),"has cleared User Cache in",getClass().getSimpleName());
464 sb.append("Now cleared\n");