diff options
author | Dan Timoney <dtimoney@att.com> | 2023-06-19 16:05:33 -0400 |
---|---|---|
committer | Dan Timoney <dtimoney@att.com> | 2023-08-10 14:48:32 -0400 |
commit | 6a7f13fa7e284cbec5b0743c10fdd33286aaf2ec (patch) | |
tree | 42fa7649ca8f912af8d47f35bad4b284369fae9d /cadi/core/src/main | |
parent | a76d3f0de616b542baea9360e80a921f5f028a78 (diff) |
Port to java 17
Update to java 17 / springboot 3 to align with OpenDaylight Argon.
Copied and ported CADI library from AAF
Issue-ID: CCSDK-3917
Signed-off-by: Dan Timoney <dtimoney@att.com>
Change-Id: Idecb0cf43c48ccbbc0c61bf4278b87a37f92a56e
Diffstat (limited to 'cadi/core/src/main')
110 files changed, 14424 insertions, 0 deletions
diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/AES.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/AES.java new file mode 100644 index 00000000..55e148a9 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/AES.java @@ -0,0 +1,131 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.onap.ccsdk.apps.cadi.Symm.Encryption; +import org.onap.ccsdk.apps.cadi.util.Chmod; + + +/** + * AES Class wraps Cipher AES, 128 + * NOTE: While not explicitly stated in JavaDocs, Ciphers AND SecretKeySpecs are NOT ThreadSafe + * Ciphers take time to create, therefore, we have pooled them. + * + * @author Jonathan + * + */ +public class AES implements Encryption { + public static final String AES = AES.class.getSimpleName(); + public static final int AES_KEY_SIZE = 128; // 256 isn't supported on all JDKs. + + private SecretKeySpec aeskeySpec; + + public static SecretKey newKey() throws NoSuchAlgorithmException { + KeyGenerator kgen = KeyGenerator.getInstance(AES); + kgen.init(AES_KEY_SIZE); + return kgen.generateKey(); + } + + public AES(byte[] aeskey, int offset, int len){ + aeskeySpec = new SecretKeySpec(aeskey,offset,len,AES); + } + + public byte[] encrypt(byte[] in) throws CadiException { + try { + Cipher c = Cipher.getInstance(AES); + c.init(Cipher.ENCRYPT_MODE,aeskeySpec); + return c.doFinal(in); + } catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new CadiException(e); + } + } + + public byte[] decrypt(byte[] in) throws CadiException { + try { + Cipher c = Cipher.getInstance(AES); + c.init(Cipher.DECRYPT_MODE,aeskeySpec); + return c.doFinal(in); + } catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new CadiException(e); + } + } + + public void save(File keyfile) throws IOException { + FileOutputStream fis = new FileOutputStream(keyfile); + try { + fis.write(aeskeySpec.getEncoded()); + } finally { + fis.close(); + } + Chmod.to400.chmod(keyfile); + } + + public CipherOutputStream outputStream(OutputStream os, boolean encrypt) { + try { + Cipher c = Cipher.getInstance(AES); + if (encrypt) { + c.init(Cipher.ENCRYPT_MODE,aeskeySpec); + } else { + c.init(Cipher.DECRYPT_MODE,aeskeySpec); + } + return new CipherOutputStream(os,c); + } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException e) { + // Cannot add Exception to this API. throw Runtime + System.err.println("Error creating Aes CipherOutputStream"); + return null; // should never get here. + } + } + + public CipherInputStream inputStream(InputStream is, boolean encrypt) { + try { + Cipher c = Cipher.getInstance(AES); + if (encrypt) { + c.init(Cipher.ENCRYPT_MODE,aeskeySpec); + } else { + c.init(Cipher.DECRYPT_MODE,aeskeySpec); + } + return new CipherInputStream(is,c); + } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException e) { + // Cannot add Exception to this API. throw Runtime + System.err.println("Error creating Aes CipherInputStream"); + return null; // should never get here. + } + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/AbsUserCache.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/AbsUserCache.java new file mode 100644 index 00000000..b8fd7e4e --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/AbsUserCache.java @@ -0,0 +1,467 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; + +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.CachedPrincipal.Resp; +import org.onap.ccsdk.apps.cadi.principal.CachedBasicPrincipal; + +/** + * Implement Fast lookup and Cache for Local User Info + * + * Include ability to add and remove Users + * + * Also includes a Timer Thread (when necessary) to invoke cleanup on expiring Credentials + * + * @author Jonathan + * + */ +public abstract class AbsUserCache<PERM extends Permission> { + // Need an obvious key for when there is no Authentication Cred + private static final String NO_CRED = "NoCred"; + static final int MIN_INTERVAL = 1000*60; // Min 1 min + static final int MAX_INTERVAL = 1000*60*60*4; // 4 hour max + private static Timer timer; + // Map of userName to User + private final Map<String, User<PERM>> userMap; + private static final Map<String, Miss> missMap = new TreeMap<>(); + private final Symm missEncrypt; + + private Clean clean; + protected Access access; + + protected AbsUserCache(Access access, long cleanInterval, int highCount, int usageCount) { + this.access = access; + Symm s; + try { + byte[] gennedKey = Symm.keygen(); + s = Symm.obtain(new ByteArrayInputStream(gennedKey)); + } catch (IOException e) { + access.log(e); + s = Symm.base64noSplit; + } + missEncrypt = s; + + userMap = new ConcurrentHashMap<>(); + + + if (cleanInterval>0) { + cleanInterval = Math.max(MIN_INTERVAL, cleanInterval); + synchronized(AbsUserCache.class) { // Lazy instantiate.. in case there is no cleanup needed + if (timer==null) { + timer = new Timer("CADI Cleanup Timer",true); + } + + timer.schedule(clean = new Clean(access, cleanInterval, highCount, usageCount), cleanInterval, cleanInterval); + access.log(Access.Level.INIT, "Cleaning Thread initialized with interval of",cleanInterval, "ms and max objects of", highCount); + } + } + } + + @SuppressWarnings("unchecked") + public AbsUserCache(AbsUserCache<PERM> cache) { + this.access = cache.access; + userMap = cache.userMap; + missEncrypt = cache.missEncrypt; + + synchronized(AbsUserCache.class) { + if (cache.clean!=null && cache.clean.lur==null && this instanceof CachingLur) { + cache.clean.lur=(CachingLur<PERM>)this; + } + } + } + + protected void setLur(CachingLur<PERM> lur) { + if (clean!=null)clean.lur = lur; + + } + + protected void addUser(User<PERM> user) { + Principal p = user.principal; + String key; + try { + if (p instanceof GetCred) { + key = missKey(p.getName(), ((GetCred)p).getCred()); + } else { + byte[] cred; + if ((cred=user.getCred())==null) { + key = user.name + NO_CRED; + } else { + key = missKey(user.name,cred); + } + } + } catch (IOException e) { + access.log(e); + return; + } + userMap.put(key, user); + } + + // Useful for looking up by WebToken, etc. + protected void addUser(String key, User<PERM> user) { + userMap.put(key, user); + } + + /** + * Add miss to missMap. If Miss exists, or too many tries, returns false. + * + * otherwise, returns true to allow another attempt. + * + * @param key + * @param bs + * @return + * @throws IOException + */ + protected synchronized boolean addMiss(String key, byte[] bs) { + String mkey; + try { + mkey = missKey(key,bs); + } catch (IOException e) { + access.log(e); + return false; + } + Miss miss = missMap.get(mkey); + if (miss==null) { + missMap.put(mkey, new Miss(bs,clean==null?MIN_INTERVAL:clean.timeInterval,key)); + return true; + } + return miss.mayContinue(); + } + + protected Miss missed(String key, byte[] bs) throws IOException { + return missMap.get(missKey(key,bs)); + } + + protected User<PERM> getUser(Principal principal) { + String key; + if (principal instanceof GetCred) { + GetCred gc = (GetCred)principal; + try { + key = missKey(principal.getName(), gc.getCred()); + } catch (IOException e) { + access.log(e, "Error getting key from Principal"); + key = principal.getName(); + } + } else { + key = principal.getName()+NO_CRED; + } + User<PERM> u = userMap.get(key); + if (u!=null) { + u.incCount(); + } + return u; + } + + protected User<PERM> getUser(CachedBasicPrincipal cbp) { + return getUser(cbp.getName(), cbp.getCred()); + } + + protected User<PERM> getUser(String user, byte[] cred) { + User<PERM> u; + String key=null; + try { + key =missKey(user,cred); + } catch (IOException e) { + access.log(e); + return null; + } + u = userMap.get(key); + if (u!=null) { + if (u.permExpired()) { + userMap.remove(key); + u=null; + } else { + u.incCount(); + } + } + return u; + } + + /** + * Removes User from the Cache + * @param user + */ + protected void remove(User<PERM> user) { + userMap.remove(user.principal.getName()); + } + + /** + * Removes user from the Cache + * + * @param user + */ + public void remove(String user) { + Object o = userMap.remove(user); + if (o!=null) { + access.log(Level.INFO, user,"removed from Client Cache by Request"); + } + } + + /** + * Clear all Users from the Client Cache + */ + public void clearAll() { + userMap.clear(); + } + + public final List<DumpInfo> dumpInfo() { + List<DumpInfo> rv = new ArrayList<>(); + for (User<PERM> user : userMap.values()) { + rv.add(new DumpInfo(user)); + } + return rv; + } + + /** + * The default behavior of a LUR is to not handle something exclusively. + */ + public boolean handlesExclusively(Permission ... pond) { + return false; + } + + /** + * Container calls when cleaning up... + * + * If overloading in Derived class, be sure to call "super.destroy()" + */ + public void destroy() { + if (timer!=null) { + timer.purge(); + timer.cancel(); + } + } + + + + // Simple map of Group name to a set of User Names + // private Map<String, Set<String>> groupMap = new HashMap<>(); + + /** + * Class to hold a small subset of the data, because we don't want to expose actual Permission or User Objects + */ + public final class DumpInfo { + public String user; + public List<String> perms; + + public DumpInfo(User<PERM> user) { + this.user = user.principal.getName(); + perms = new ArrayList<>(user.perms.keySet()); + } + } + + /** + * Clean will examine resources, and remove those that have expired. + * + * If "highs" have been exceeded, then we'll expire 10% more the next time. This will adjust after each run + * without checking contents more than once, making a good average "high" in the minimum speed. + * + * @author Jonathan + * + */ + private final class Clean extends TimerTask { + private final Access access; + private CachingLur<PERM> lur; + + // The idea here is to not be too restrictive on a high, but to Expire more items by + // shortening the time to expire. This is done by judiciously incrementing "advance" + // when the "highs" are exceeded. This effectively reduces numbers of cached items quickly. + private final int high; + private long advance; + private final long timeInterval; + private final int usageTriggerCount; + + public Clean(Access access, long cleanInterval, int highCount, int usageTriggerCount) { + this.access = access; + lur = null; + high = highCount; + timeInterval = cleanInterval; + advance = 0; + this.usageTriggerCount=usageTriggerCount; + } + public void run() { + int renewed = 0; + int count = 0; + int total = 0; + try { + // look at now. If we need to expire more by increasing "now" by "advance" + ArrayList<User<PERM>> al = new ArrayList<>(userMap.values().size()); + al.addAll(0, userMap.values()); + long now = System.currentTimeMillis() + advance; + for (User<PERM> user : al) { + ++total; + if (user.count>usageTriggerCount) { + boolean touched = false, removed=false; + if (user.principal instanceof CachedPrincipal) { + CachedPrincipal cp = (CachedPrincipal)user.principal; + if (cp.expires() < now) { + switch(cp.revalidate(null)) { + case INACCESSIBLE: + access.log(Level.AUDIT, "AAF Inaccessible. Keeping credentials"); + break; + case REVALIDATED: + user.resetCount(); + touched = true; + break; + default: + user.resetCount(); + remove(user); + ++count; + removed = true; + break; + } + } + } + + if (!removed && lur!=null && user.permExpires<= now ) { + if (lur.reload(user).equals(Resp.REVALIDATED)) { + user.renewPerm(); + access.log(Level.DEBUG, "Reloaded Perms for",user); + touched = true; + } + } + user.resetCount(); + if (touched) { + ++renewed; + } + + } else { + if (user.permExpired()) { + remove(user); + ++count; + } + } + } + + // Clean out Misses + int missTotal = missMap.keySet().size(); + int miss = 0; + if (missTotal>0) { + ArrayList<String> keys = new ArrayList<>(missTotal); + keys.addAll(missMap.keySet()); + for (String key : keys) { + Miss m = missMap.get(key); + if (m!=null) { + long timeLeft = m.timestamp - System.currentTimeMillis(); + if (timeLeft<0) { + synchronized(missMap) { + missMap.remove(key); + } + access.log(Level.INFO, m.name, " has been removed from Missed Credential Map (" + m.tries + " invalid tries)"); + ++miss; + } else { + access.log(Level.INFO, m.name, " remains in Missed Credential Map (" + m.tries + " invalid tries) for " + (timeLeft/1000) + " more seconds"); + } + } + } + } + + if (count+renewed+miss>0) { + access.log(Level.INFO, (lur==null?"Cache":lur.getClass().getSimpleName()), "removed",count, + "and renewed",renewed,"expired Permissions out of", total,"and removed", miss, "password misses out of",missTotal); + } + + // If High (total) is reached during this period, increase the number of expired services removed for next time. + // There's no point doing it again here, as there should have been cleaned items. + if (total>high) { + // advance cleanup by 10%, without getting greater than timeInterval. + advance = Math.min(timeInterval, advance+(timeInterval/10)); + } else { + // reduce advance by 10%, without getting lower than 0. + advance = Math.max(0, advance-(timeInterval/10)); + } + } catch (Exception e) { + access.log(Level.ERROR,e.getMessage()); + } + } + } + + + private String missKey(String name, byte[] bs) throws IOException { + return name + Hash.toHex(missEncrypt.encode(bs)); + } + + protected static class Miss { + private static final int MAX_TRIES = 3; + + long timestamp; + + private long timetolive; + + private long tries; + + private final String name; + + public Miss(final byte[] first, final long timeInterval, final String name) { + timestamp = System.currentTimeMillis() + timeInterval; + this.timetolive = timeInterval; + tries = 0L; + this.name = name; + } + + + public synchronized boolean mayContinue() { + long ts = System.currentTimeMillis(); + if (ts>timestamp) { + tries = 0; + timestamp = ts + timetolive; + } else if (MAX_TRIES <= ++tries) { + return false; + } + return true; + } + + } + + /** + * Report on state + */ + public String toString() { + return getClass().getSimpleName() + + " Cache:\n Users Cached: " + + userMap.size() + + "\n Misses Saved: " + + missMap.size() + + '\n'; + + } + + public void clear(Principal p, StringBuilder sb) { + sb.append(toString()); + userMap.clear(); + missMap.clear(); + access.log(Level.AUDIT, p.getName(),"has cleared User Cache in",getClass().getSimpleName()); + sb.append("Now cleared\n"); + } + +}
\ No newline at end of file diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Access.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Access.java new file mode 100644 index 00000000..1d28405b --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Access.java @@ -0,0 +1,180 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * Various Environments require different logging mechanisms, or at least allow + * for different ones. We need the Framework to be able to hook into any particular instance of logging + * mechanism, whether it be a Logging Object within a Servlet Context, or a direct library like log4j. + * This interface, therefore, allows maximum pluggability in a variety of different app styles. + * + * + * + */ +public interface Access { + // levels to use + public enum Level { + DEBUG(0x1), INFO(0x10), AUDIT(0x100), WARN(0x2000), ERROR(0x4000), INIT(0x8000),TRACE(0x10000),NONE(0XFFFF); + private final int bit; + + Level(int ord) { + bit = ord; + } + + public boolean inMask(int mask) { + return (mask & bit) == bit; + } + + public int addToMask(int mask) { + return mask | bit; + } + + public int delFromMask(int mask) { + return mask & ~bit; + } + + public int toggle(int mask) { + if (inMask(mask)) { + return delFromMask(mask); + } else { + return addToMask(mask); + } + } + + + public int maskOf() { + int mask=0; + for (Level l : values()) { + if (ordinal()<=l.ordinal() && l!=NONE) { + mask|=l.bit; + } + } + return mask; + } + } + + /** + * Write a variable list of Object's text via the toString() method with appropriate space, etc. + * @param elements + */ + public void log(Level level, Object ... elements); + + /** + * Printf mechanism for Access + * @param level + * @param fmt + * @param elements + */ + public void printf(Level level, String fmt, Object ... elements); + + /** + * Check if message will log before constructing + * @param level + * @return + */ + public boolean willLog(Level level); + + /** + * Write the contents of an exception, followed by a variable list of Object's text via the + * toString() method with appropriate space, etc. + * + * The Loglevel is always "ERROR" + * + * @param elements + */ + public void log(Exception e, Object ... elements); + + /** + * Set the Level to compare logging too + */ + public void setLogLevel(Level level); + + /** + * It is important in some cases to create a class from within the same Classloader that created + * Security Objects. Specifically, it's pretty typical for Web Containers to separate classloaders + * so as to allow Apps with different dependencies. + * @return + */ + public ClassLoader classLoader(); + + public String getProperty(String string, String def); + + public Properties getProperties(); + + public void load(InputStream is) throws IOException; + + /** + * if "anytext" is true, then decryption will always be attempted. Otherwise, only if starts with + * Symm.ENC + * @param encrypted + * @param anytext + * @return + * @throws IOException + */ + public String decrypt(String encrypted, boolean anytext) throws IOException; + + public static final Access NULL = new Access() { + public void log(Level level, Object... elements) { + } + + @Override + public void printf(Level level, String fmt, Object... elements) { + } + + public void log(Exception e, Object... elements) { + } + + public ClassLoader classLoader() { + return ClassLoader.getSystemClassLoader(); + } + + public String getProperty(String string, String def) { + return null; + } + + public void load(InputStream is) throws IOException { + } + + public void setLogLevel(Level level) { + } + + public String decrypt(String encrypted, boolean anytext) throws IOException { + return encrypted; + } + + @Override + public boolean willLog(Level level) { + return false; + } + + @Override + public Properties getProperties() { + return new Properties(); + } + }; + + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/BasicCred.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/BasicCred.java new file mode 100644 index 00000000..37c5a2ed --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/BasicCred.java @@ -0,0 +1,35 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +/** + * An Interface for testing on Requests to see if we can get a User and Password + * It works for CadiWrap, but also, Container Specific Wraps (aka Tomcat) should also + * implement. + * + * + */ +public interface BasicCred extends GetCred { + public void setUser(String user); + public void setCred(byte[] passwd); + public String getUser(); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/BufferedServletInputStream.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/BufferedServletInputStream.java new file mode 100644 index 00000000..86ae1900 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/BufferedServletInputStream.java @@ -0,0 +1,223 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.io.IOException; +import java.io.InputStream; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; + +/** + * BufferedServletInputStream + * + * There are cases in brain-dead middleware (SOAP) where they store routing information in the content. + * + * In HTTP, this requires reading the content from the InputStream which, of course, cannot be re-read. + * + * BufferedInputStream exists to implement the "Mark" protocols for Streaming, which will enable being + * re-read. Unfortunately, J2EE chose to require a "ServletInputStream" as an abstract class, rather than + * an interface, which requires we create a delegating pattern, rather than the preferred inheriting pattern. + * + * Unfortunately, the standard "BufferedInputStream" cannot be used, because it simply creates a byte array + * in the "mark(int)" method of that size. This is not appropriate for this application, because the Header + * can be potentially huge, and if a buffer was allocated to accommodate all possibilities, the cost of memory + * allocation would be too large for high performance transactions. + * + * + * + */ +public class BufferedServletInputStream extends ServletInputStream { + private static final int NONE = 0; + private static final int STORE = 1; + private static final int READ = 2; + + private InputStream is; + private int state = NONE; + private Capacitor capacitor; + + public BufferedServletInputStream(InputStream is) { + this.is = is; + capacitor = null; + } + + + public int read() throws IOException { + int value=-1; + if (capacitor==null) { + value=is.read(); + } else { + switch(state) { + case STORE: + value = is.read(); + if (value>=0) { + capacitor.put((byte)value); + } + break; + case READ: + value = capacitor.read(); + if (value<0) { + capacitor.done(); + capacitor=null; // all done with buffer + value = is.read(); + } + } + } + return value; + } + @Override + public int read(byte[] b) throws IOException { + return read(b,0,b.length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int count = -1; + if (capacitor==null) { + count = is.read(b,off,len); + } else { + switch(state) { + case STORE: + count = is.read(b, off, len); + if (count>0) { + capacitor.put(b, off, count); + } + break; + case READ: + count = capacitor.read(b, off, len); + if (count<=0) { + capacitor.done(); + capacitor=null; // all done with buffer + } + if (count<len) { + int temp = is.read(b, count, len-count); + if (temp>0) { // watch for -1 + count+=temp; + } else if (count<=0) { + count = temp; // must account for Stream coming back -1 + } + } + break; + } + } + return count; + } + + public long skip(long n) throws IOException { + long skipped = capacitor.skip(n); + if (skipped<n) { + skipped += is.skip(n-skipped); + } + return skipped; + } + + + public int available() throws IOException { + int count = is.available(); + if (capacitor!=null)count+=capacitor.available(); + return count; + } + + /** + * Return just amount buffered (for debugging purposes, mostly) + * @return + */ + public int buffered() { + return capacitor.available(); + } + + + public void close() throws IOException { + if (capacitor!=null) { + capacitor.done(); + capacitor=null; + } + is.close(); + } + + + /** + * Note: Readlimit is ignored in this implementation, because the need was for unknown buffer size which wouldn't + * require allocating and dumping huge chunks of memory every use, or risk overflow. + */ + public synchronized void mark(int readlimit) { + switch(state) { + case NONE: + capacitor = new Capacitor(); + break; + case READ: + capacitor.done(); + break; + } + state = STORE; + } + + + /** + * Reset Stream + * + * Calling this twice is not supported in typical Stream situations, but it is allowed in this service. The caveat is that it can only reset + * the data read in since Mark has been called. The data integrity is only valid if you have not continued to read past what is stored. + * + */ + public synchronized void reset() throws IOException { + switch(state) { + case STORE: + capacitor.setForRead(); + state = READ; + break; + case READ: + capacitor.reset(); + break; + case NONE: + throw new IOException("InputStream has not been marked"); + } + } + + + public boolean markSupported() { + return true; + } + + + @Override + public boolean isFinished() { + + try { + return (this.is.available() == 0); + } catch (IOException e) { + return true; + } + } + + + @Override + public boolean isReady() { + return true; + } + + + @Override + public void setReadListener(ReadListener arg0) { + throw new UnsupportedOperationException("Unimplemented method 'setReadListener'"); + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CachedPrincipal.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CachedPrincipal.java new file mode 100644 index 00000000..6af19e6d --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CachedPrincipal.java @@ -0,0 +1,46 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.security.Principal; + +/** + * Cached Principals need to be able to revalidate in the background. + * + * + */ +public interface CachedPrincipal extends Principal { + public enum Resp {NOT_MINE,UNVALIDATED,REVALIDATED,INACCESSIBLE,DENIED}; + + /** + * Re-validate with Creator + * + * @return + */ + public abstract Resp revalidate(Object state); + + /** + * Store when last updated. + * @return + */ + public abstract long expires(); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CachingLur.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CachingLur.java new file mode 100644 index 00000000..5b3ac9d0 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CachingLur.java @@ -0,0 +1,34 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.security.Principal; + +import org.onap.ccsdk.apps.cadi.CachedPrincipal.Resp; + + +public interface CachingLur<PERM extends Permission> extends Lur { + public abstract void remove(String user); + public abstract Resp reload(User<PERM> user); + public abstract void setDebug(String commaDelimIDsOrNull); + public abstract void clear(Principal p, StringBuilder sb); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CadiException.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CadiException.java new file mode 100644 index 00000000..c0dfce93 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CadiException.java @@ -0,0 +1,49 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +/** + * CADI Specific Exception + */ +public class CadiException extends Exception { + /** + * Generated ID + */ + private static final long serialVersionUID = -4180145363107742619L; + + public CadiException() { + super(); + } + + public CadiException(String message) { + super(message); + } + + public CadiException(Throwable cause) { + super(cause); + } + + public CadiException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CadiWrap.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CadiWrap.java new file mode 100644 index 00000000..495076bf --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CadiWrap.java @@ -0,0 +1,200 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; + +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.filter.NullPermConverter; +import org.onap.ccsdk.apps.cadi.filter.PermConverter; +import org.onap.ccsdk.apps.cadi.lur.EpiLur; +import org.onap.ccsdk.apps.cadi.principal.TaggedPrincipal; +import org.onap.ccsdk.apps.cadi.taf.TafResp; +import org.onap.ccsdk.apps.cadi.util.Timing; + + + +/** + * Inherit the HttpServletRequestWrapper, which calls methods of delegate it's created with, but + * overload the key security mechanisms with CADI mechanisms + * + * This works with mechanisms working strictly with HttpServletRequest (i.e. Servlet Filters) + * + * Specialty cases, i.e. Tomcat, which for their containers utilize their own mechanisms and Wrappers, you may + * need something similar. See AppServer specific code (i.e. tomcat) for these. + * + * @author Jonathan + * + */ +public class CadiWrap extends HttpServletRequestWrapper implements HttpServletRequest, BasicCred { + private TaggedPrincipal principal; + private Lur lur; + private String user; // used to set user/pass from brain-dead protocols like WSSE + private byte[] password; + private PermConverter pconv; + private Access access; + + /** + * Standard Wrapper constructor for Delegate pattern + * @param request + */ + public CadiWrap(HttpServletRequest request, TafResp tafResp, Lur lur) { + super(request); + principal = tafResp.getPrincipal(); + access = tafResp.getAccess(); + this.lur = lur; + pconv = NullPermConverter.singleton(); + } + + /** + * Standard Wrapper constructor for Delegate pattern, with PermConverter + * @param request + */ + public CadiWrap(HttpServletRequest request, TafResp tafResp, Lur lur, PermConverter pc) { + super(request); + principal = tafResp.getPrincipal(); + access = tafResp.getAccess(); + this.lur = lur; + pconv = pc; + } + + + /** + * Part of the HTTP Security API. Declare the User associated with this HTTP Transaction. + * CADI does this by reporting the name associated with the Principal obtained, if any. + */ + @Override + public String getRemoteUser() { + return principal==null?null:principal.getName(); + } + + /** + * Part of the HTTP Security API. Return the User Principal associated with this HTTP + * Transaction. + */ + @Override + public Principal getUserPrincipal() { + return principal; + } + + /** + * This is the key API call for AUTHZ in J2EE. Given a Role (String passed in), is the user + * associated with this HTTP Transaction allowed to function in this Role? + * + * For CADI, we pass the responsibility for determining this to the "LUR", which may be + * determined by the Enterprise. + * + * Note: Role check is also done in "CadiRealm" in certain cases... + * + * + */ + @Override + public boolean isUserInRole(String perm) { + return perm==null?false:checkPerm(access,"isUserInRole",principal,pconv,lur,perm); + } + + public static boolean checkPerm(Access access, String caller, Principal principal, PermConverter pconv, Lur lur, String perm) { + if (principal== null) { + access.log(Level.AUDIT,caller, "No Principal in Transaction"); + return false; + } else { + final long start = System.nanoTime(); + perm = pconv.convert(perm); + if (lur.fish(principal,lur.createPerm(perm))) { + access.printf(Level.DEBUG,"%s: %s has %s, %f ms", caller, principal.getName(), perm, Timing.millis(start)); + return true; + } else { + access.printf(Level.DEBUG,"%s: %s does not have %s, %f ms", caller, principal.getName(), perm, Timing.millis(start)); + return false; + } + } + + } + + /** + * CADI Function (Non J2EE standard). GetPermissions will read the Permissions from AAF (if configured) and Roles from Local Lur, etc + * as implemented with lur.fishAll + * + * To utilize, the Request must be a "CadiWrap" object, then call. + */ + public List<Permission> getPermissions(Principal p) { + List<Permission> perms = new ArrayList<>(); + lur.fishAll(p, perms); + return perms; + } + /** + * Allow setting of tafResp and lur after construction + * + * This can happen if the CadiWrap is constructed in a Valve other than CadiValve + */ + public void set(TafResp tafResp, Lur lur) { + principal = tafResp.getPrincipal(); + access = tafResp.getAccess(); + this.lur = lur; + } + + public String getUser() { + if (user==null && principal!=null) { + user = principal.getName(); + } + return user; + } + + public byte[] getCred() { + return password; + } + + public void setUser(String user) { + this.user = user; + } + + public void setCred(byte[] passwd) { + password = passwd; + } + + public CadiWrap setPermConverter(PermConverter pc) { + pconv = pc; + return this; + } + + // Add a feature + public void invalidate(String id) { + if (lur instanceof EpiLur) { + ((EpiLur)lur).remove(id); + } else if (lur instanceof CachingLur) { + ((CachingLur<?>)lur).remove(id); + } + } + + public Lur getLur() { + return lur; + } + + public Access access() { + return access; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Capacitor.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Capacitor.java new file mode 100644 index 00000000..2d8a7229 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Capacitor.java @@ -0,0 +1,241 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.nio.ByteBuffer; +import java.util.ArrayList; + +/** + * Capacitor + * + * Storage mechanism for read data, specifically designed for InputStreams. + * + * The Standard BufferedInputStream requires a limit to be set for buffered reading, which is + * impractical for reading SOAP headers, which can be quite large. + * @author Jonathan + * + */ +public class Capacitor { + private static final int DEFAULT_CHUNK = 256; + private ArrayList<ByteBuffer> bbs = new ArrayList<>(); + private ByteBuffer curr = null; + private int idx; + + // Maintain a private RingBuffer for Memory, for efficiency + private static ByteBuffer[] ring = new ByteBuffer[16]; + private static int start, end; + + + public void put(byte b) { + if (curr == null || curr.remaining()==0) { // ensure we have a "curr" buffer ready for data + curr = ringGet(); + bbs.add(curr); + } + curr.put(b); + } + + public int read() { + if (curr!=null) { + if (curr.remaining()>0) { // have a buffer, use it! + return curr.get(); + } else if (idx<bbs.size()){ // Buffer not enough, get next one from array + curr=bbs.get(idx++); + return curr.get(); + } + } // if no curr buffer, treat as end of stream + return -1; + } + + /** + * read into an array like Streams + * + * @param array + * @param offset + * @param length + * @return + */ + public int read(byte[] array, int offset, int length) { + if (curr==null)return -1; + int len; + int count=0; + while (length>0) { // loop through while there's data needed + if ((len=curr.remaining())>length) { // if enough data in curr buffer, use this code + curr.get(array,offset,length); + count+=length; + length=0; + } else { // get data from curr, mark how much is needed to fulfil, and loop for next curr. + curr.get(array,offset,len); + count+=len; + offset+=len; + length-=len; + if (idx<bbs.size()) { + curr=bbs.get(idx++); + } else { + length=0; // stop, and return the count of how many we were able to load + } + } + } + return count; + } + + /** + * Put an array of data into Capacitor + * + * @param array + * @param offset + * @param length + */ + public void put(byte[] array, int offset, int length) { + if (curr == null || curr.remaining()==0) { + curr = ringGet(); + bbs.add(curr); + } + + int len; + while (length>0) { + if ((len=curr.remaining())>length) { + curr.put(array,offset,length); + length=0; + } else { +// System.out.println(new String(array)); + curr.put(array,offset,len); + length-=len; + offset+=len; + curr = ringGet(); + bbs.add(curr); + } + } + } + + /** + * Move state from Storage mode into Read mode, changing all internal buffers to read mode, etc + */ + public void setForRead() { + for (ByteBuffer bb : bbs) { + bb.flip(); + } + if (bbs.isEmpty()) { + curr = null; + idx = 0; + } else { + curr=bbs.get(0); + idx=1; + } + } + + /** + * reuse all the buffers + */ + public void done() { + for (ByteBuffer bb : bbs) { + ringPut(bb); + } + bbs.clear(); + curr = null; + } + + /** + * Declare amount of data available to be read at once. + * + * @return + */ + public int available() { + int count = 0; + for (ByteBuffer bb : bbs) { + count+=bb.remaining(); + } + return count; + } + + /** + * Returns how many are left that were not skipped + * @param n + * @return + */ + public long skip(long n) { + long skipped=0L; + int skip; + if (curr==null) { + return 0; + } + while (n>0) { + if (n<(skip=curr.remaining())) { + curr.position(curr.position()+(int)n); + skipped+=skip; + n=0; + } else { + curr.position(curr.limit()); + + skipped-=skip; + if (idx<bbs.size()) { + curr=bbs.get(idx++); + n-=skip; + } else { + n=0; + } + } + } + return skipped > 0 ? skipped : 0; + } + /** + * Be able to re-read data that is stored that has already been re-read. This is not a standard Stream behavior, but can be useful + * in a standalone mode. + */ + public void reset() { + for (ByteBuffer bb : bbs) { + bb.position(0); + } + if (bbs.isEmpty()) { + curr = null; + idx = 0; + } else { + curr=bbs.get(0); + idx=1; + } + } + + /* + * Ring Functions. Reuse allocated memory + */ + private ByteBuffer ringGet() { + ByteBuffer bb = null; + synchronized(ring) { + bb=ring[start]; + ring[start]=null; + if (bb!=null && ++start>15)start=0; + } + if (bb==null) { + bb=ByteBuffer.allocate(DEFAULT_CHUNK); + } else { + bb.clear();// refresh reused buffer + } + return bb; + } + + private void ringPut(ByteBuffer bb) { + synchronized(ring) { + ring[end]=bb; // if null or not, BB will just be Garbage collected + if (++end>15)end=0; + } + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CmdLine.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CmdLine.java new file mode 100644 index 00000000..f4ac0f4e --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CmdLine.java @@ -0,0 +1,357 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.security.NoSuchAlgorithmException; + +import org.onap.ccsdk.apps.cadi.util.Chmod; +import org.onap.ccsdk.apps.cadi.util.JsonOutputStream; + + + +/** + * A Class to run on command line to determine suitability of environment for certain TAFs. + * * + * @author Jonathan + * + */ +public class CmdLine { + + private static boolean systemExit = true; + /** + * @param args + */ + public static void main(String[] args) { + if (args.length>0) { + if ("digest".equalsIgnoreCase(args[0]) && (args.length>2 || (args.length>1 && System.console()!=null))) { + String keyfile; + String password; + if (args.length>2) { + password = args[1]; + keyfile = args[2]; + if ("-i".equals(password)) { + int c; + StringBuilder sb = new StringBuilder(); + try { + while ((c=System.in.read())>=0) { + sb.append((char)c); + } + } catch (IOException e) { + e.printStackTrace(); + } + password = sb.toString(); + } + } else { + keyfile = args[1]; + password = new String(System.console().readPassword("Type here (keystrokes hidden): ")); + } + + try { + Symm symm; + FileInputStream fis = new FileInputStream(keyfile); + try { + symm = Symm.obtain(fis); + } finally { + fis.close(); + } + symm.enpass(password, System.out); + System.out.println(); + System.out.flush(); + return; + /* testing code... don't want it exposed + System.out.println(" ******** Testing *********"); + for (int i=0;i<100000;++i) { + System.out.println(args[1]); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + b64.enpass(args[1], baos); + String pass; + System.out.println(pass=new String(baos.toByteArray())); + ByteArrayOutputStream reconstituted = new ByteArrayOutputStream(); + b64.depass(pass, reconstituted); + String r = reconstituted.toString(); + System.out.println(r); + if (!r.equals(args[1])) { + System.err.println("!!!!! STOP - ERROR !!!!!"); + return; + } + System.out.println(); + } + System.out.flush(); + */ + + } catch (IOException e) { + System.err.println("Cannot digest password"); + System.err.println(" \""+ e.getMessage() + '"'); + } +// DO NOT LEAVE THIS METHOD Compiled IN CODE... Do not want looking at passwords on disk too easy +// Jonathan. Oh, well, Deployment services need this behavior. I will put this code in, but leave it undocumented. +// One still needs access to the keyfile to read. +// July 2016 - thought of a tool "CMPass" to regurgitate from properties, but only if allowed. + } else if (("regurgitate".equalsIgnoreCase(args[0]) || "undigest".equalsIgnoreCase(args[0])) + && args.length>2) { + try { + Symm symm; + FileInputStream fis = new FileInputStream(args[2]); + try { + symm = Symm.obtain(fis); + } finally { + fis.close(); + } + boolean isFile = false; + if ("-i".equals(args[1]) || (isFile="-f".equals(args[1]))) { + BufferedReader br; + if (isFile) { + if (args.length<4) { + System.err.println("Filename in 4th position"); + return; + } + br = new BufferedReader(new FileReader(args[3])); + } else { + br = new BufferedReader(new InputStreamReader(System.in)); + } + try { + String line; + boolean cont = false; + StringBuffer sb = new StringBuffer(); + JsonOutputStream jw = new JsonOutputStream(System.out); + while ((line=br.readLine())!=null) { + if (cont) { + int end; + if ((end=line.indexOf('"'))>=0) { + sb.append(line,0,end); + cont=false; + } else { + sb.append(line); + } + } else { + int idx; + if ((idx = line.indexOf(' '))>=0 + && (idx = line.indexOf(' ',++idx))>0 + && (idx = line.indexOf('=',++idx))>0 + ) { + System.out.println(line.substring(0, idx-5)); + int start = idx+2; + int end; + if ((end=line.indexOf('"',start))<0) { + end = line.length(); + cont = true; + } + sb.append(line,start,end); + } + } + if (sb.length()>0) { + symm.depass(sb.toString(),jw); + if (!cont) { + System.out.println(); + } + } + System.out.flush(); + sb.setLength(0); + if (!cont) { + jw.resetIndent(); + } + } + } finally { + if (isFile) { + br.close(); + } + } + } else { + symm.depass(args[1], System.out); + } + System.out.println(); + System.out.flush(); + return; + } catch (IOException e) { + System.err.println("Cannot undigest password"); + System.err.println(" \""+ e.getMessage() + '"'); + } + } else if ("encode64".equalsIgnoreCase(args[0]) && args.length>1) { + try { + Symm.base64.encode(args[1], System.out); + System.out.println(); + System.out.flush(); + return; + } catch (IOException e) { + System.err.println("Cannot encode Base64 with " + args[1]); + System.err.println(" \""+ e.getMessage() + '"'); + } + } else if ("decode64".equalsIgnoreCase(args[0]) && args.length>1) { + try { + Symm.base64.decode(args[1], System.out); + System.out.println(); + System.out.flush(); + return; + } catch (IOException e) { + System.err.println("Cannot decode Base64 text from " + args[1]); + System.err.println(" \""+ e.getMessage() + '"'); + } + } else if ("encode64url".equalsIgnoreCase(args[0]) && args.length>1) { + try { + Symm.base64url.encode(args[1], System.out); + System.out.println(); + System.out.flush(); + return; + } catch (IOException e) { + System.err.println("Cannot encode Base64url with " + args[1]); + System.err.println(" \""+ e.getMessage() + '"'); + } + } else if ("decode64url".equalsIgnoreCase(args[0]) && args.length>1) { + try { + Symm.base64url.decode(args[1], System.out); + System.out.println(); + System.out.flush(); + return; + } catch (IOException e) { + System.err.println("Cannot decode Base64url text from " + args[1]); + System.err.println(" \""+ e.getMessage() + '"'); + } + } else if ("md5".equalsIgnoreCase(args[0]) && args.length>1) { + try { + System.out.println(Hash.hashMD5asStringHex(args[1])); + System.out.flush(); + } catch (NoSuchAlgorithmException e) { + System.err.println("Cannot hash MD5 from " + args[1]); + System.err.println(" \""+ e.getMessage() + '"'); + } + return; + } else if ("sha256".equalsIgnoreCase(args[0]) && args.length>1) { + try { + if (args.length>2) { + int max = args.length>7?7:args.length; + for (int i=2;i<max;++i) { + int salt = Integer.parseInt(args[i]); + System.out.println(Hash.hashSHA256asStringHex(args[1],salt)); + } + } else { + System.out.println(Hash.hashSHA256asStringHex(args[1])); + } + } catch (NoSuchAlgorithmException e) { + System.err.println("Cannot hash SHA256 text from " + args[1]); + System.err.println(" \""+ e.getMessage() + '"'); + } + System.out.flush(); + return; + } else if ("keygen".equalsIgnoreCase(args[0])) { + try { + if (args.length>1) { + File f = new File(args[1]); + FileOutputStream fos = new FileOutputStream(f); + try { + fos.write(Symm.keygen()); + fos.flush(); + } finally { + fos.close(); + Chmod.to400.chmod(f); + } + } else { + // create a Symmetric Key out of same characters found in base64 + System.out.write(Symm.keygen()); + System.out.flush(); + } + return; + } catch (IOException e) { + System.err.println("Cannot create a key " + args[0]); + System.err.println(" \""+ e.getMessage() + '"'); + } + + } else if ("passgen".equalsIgnoreCase(args[0])) { + int numDigits; + if (args.length <= 1) { + numDigits = 24; + } else { + numDigits = Integer.parseInt(args[1]); + if (numDigits<8)numDigits = 8; + } + String pass; + boolean noLower,noUpper,noDigits,noSpecial,repeatingChars,missingChars; + do { + pass = Symm.randomGen(numDigits); + missingChars=noLower=noUpper=noDigits=noSpecial=true; + repeatingChars=false; + int c=-1,last; + for (int i=0;i<numDigits;++i) { + last = c; + c = pass.charAt(i); + if (c==last) { + repeatingChars=true; + break; + } + if (noLower) { + noLower=!(c>=0x61 && c<=0x7A); + } + if (noUpper) { + noUpper=!(c>=0x41 && c<=0x5A); + } + if (noDigits) { + noDigits=!(c>=0x30 && c<=0x39); + } + if (noSpecial) { + noSpecial = "+!@#$%^&*(){}[]?:;,.".indexOf(c)<0; + } + + missingChars = (noLower || noUpper || noDigits || noSpecial); + } + } while (missingChars || repeatingChars); + System.out.println(pass.substring(0,numDigits)); + } else if ("urlgen".equalsIgnoreCase(args[0])) { + int numDigits; + if (args.length <= 1) { + numDigits = 24; + } else { + numDigits = Integer.parseInt(args[1]); + } + System.out.println(Symm.randomGen(Symm.base64url.codeset, numDigits).substring(0,numDigits)); + } + } else { + System.out.println("Usage: java -jar <this jar> ..."); + System.out.println(" keygen [<keyfile>] (Generates Key on file, or Std Out)"); + System.out.println(" digest [<passwd>|-i|] <keyfile> (Encrypts Password with \"keyfile\""); + System.out.println(" if passwd = -i, will read StdIn"); + System.out.println(" if passwd is blank, will ask securely)"); + System.out.println(" undigest <enc:...> <keyfile> (Decrypts Encoded with \"keyfile\")"); + System.out.println(" passgen <digits> (Generate Password of given size)"); + System.out.println(" urlgen <digits> (Generate URL field of given size)"); + System.out.println(" encode64 <your text> (Encodes to Base64)"); + System.out.println(" decode64 <base64 encoded text> (Decodes from Base64)"); + System.out.println(" encode64url <your text> (Encodes to Base64 URL charset)"); + System.out.println(" decode64url <base64url encoded text> (Decodes from Base64 URL charset)"); + System.out.println(" sha256 <text> <salts(s)> (Digest String into SHA256 Hash)"); + System.out.println(" md5 <text> (Digest String into MD5 Hash)"); + } + if (systemExit) { + System.exit(1); + } + } + + public static void setSystemExit(boolean shouldExit) { + systemExit = shouldExit; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Connector.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Connector.java new file mode 100644 index 00000000..04e3b1db --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Connector.java @@ -0,0 +1,27 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +@FunctionalInterface +public interface Connector { + public Lur newLur() throws CadiException; +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CredVal.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CredVal.java new file mode 100644 index 00000000..ea2b7a78 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CredVal.java @@ -0,0 +1,41 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + + +/** + * UserPass + * + * The essential interface required by BasicAuth to determine if a given User/Password combination is + * valid. This is done as an interface. + * + */ +public interface CredVal { + public enum Type{PASSWORD}; + /** + * Validate if the User/Password combination matches records + * @param user + * @param pass + * @return + */ + public boolean validate(String user, Type type, byte[] cred, Object state); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CredValDomain.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CredValDomain.java new file mode 100644 index 00000000..727ac8e2 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/CredValDomain.java @@ -0,0 +1,25 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ +package org.onap.ccsdk.apps.cadi; + +public interface CredValDomain extends CredVal { + public String domain(); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/GetCred.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/GetCred.java new file mode 100644 index 00000000..647b89e4 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/GetCred.java @@ -0,0 +1,28 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + + +@FunctionalInterface +public interface GetCred { + byte[] getCred(); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Hash.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Hash.java new file mode 100644 index 00000000..09cf2de2 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Hash.java @@ -0,0 +1,266 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * + * + * + */ +public class Hash { + private static char hexDigit[] = "0123456789abcdef".toCharArray(); + +///////////////////////////////// +// MD5 +///////////////////////////////// + /** + * Encrypt MD5 from Byte Array to Byte Array + * @param input + * @return + * @throws NoSuchAlgorithmException + */ + public static byte[] hashMD5 (byte[] input) throws NoSuchAlgorithmException { + // Note: Protect against Multi-thread issues with new MessageDigest + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(input); + return md.digest(); + } + + /** + * Encrypt MD5 from Byte Array to Byte Array + * @param input + * @return + * @throws NoSuchAlgorithmException + */ + public static byte[] hashMD5 (byte[] input, int offset, int length) throws NoSuchAlgorithmException { + // Note: Protect against Multi-thread issues with new MessageDigest + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(input,offset,length); + return md.digest(); + } + + + + /** + * Convenience Function: Encrypt MD5 from String to String Hex representation + * + * @param input + * @return + * @throws NoSuchAlgorithmException + */ + public static String hashMD5asStringHex(String input) throws NoSuchAlgorithmException { + byte[] output = hashMD5(input.getBytes()); + StringBuilder sb = new StringBuilder("0x"); + for (byte b : output) { + sb.append(hexDigit[(b >> 4) & 0x0f]); + sb.append(hexDigit[b & 0x0f]); + } + return sb.toString(); + } + +///////////////////////////////// +// SHA256 +///////////////////////////////// + /** + * SHA256 Hashing + */ + public static byte[] hashSHA256(byte[] input) throws NoSuchAlgorithmException { + // Note: Protect against Multi-thread issues with new MessageDigest + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(input); + return md.digest(); + } + + /** + * SHA256 Hashing + */ + public static byte[] hashSHA256(byte[] input, int offset, int length) throws NoSuchAlgorithmException { + // Note: Protect against Multi-thread issues with new MessageDigest + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(input,offset,length); + return md.digest(); + } + + /** + * Convenience Function: Hash from String to String Hex representation + * + * @param input + * @return + * @throws NoSuchAlgorithmException + */ + public static String hashSHA256asStringHex(String input) throws NoSuchAlgorithmException { + return toHex(hashSHA256(input.getBytes())); + } + + /** + * Convenience Function: Hash from String to String Hex representation + * + * @param input + * @return + * @throws NoSuchAlgorithmException + */ + public static String hashSHA256asStringHex(String input, int salt) throws NoSuchAlgorithmException { + byte[] in = input.getBytes(); + ByteBuffer bb = ByteBuffer.allocate(Integer.SIZE + in.length); + bb.putInt(salt); + bb.put(input.getBytes()); + return toHex(Hash.hashSHA256(bb.array())); + } + + /** + * Compare two byte arrays for equivalency + * @param ba1 + * @param ba2 + * @return + */ + public static boolean isEqual(byte ba1[], byte ba2[]) { + if (ba1.length!=ba2.length)return false; + for (int i = 0;i<ba1.length; ++i) { + if (ba1[i]!=ba2[i])return false; + } + return true; + } + + public static int compareTo(byte[] a, byte[] b) { + int end = Math.min(a.length, b.length); + int compare = 0; + for (int i=0;compare == 0 && i<end;++i) { + compare = a[i]-b[i]; + } + if (compare==0)compare=a.length-b.length; + return compare; + } + + /** + * @param ba + * @return + */ + public static String toHexNo0x(byte[] ba) { + StringBuilder sb = new StringBuilder(); + for (byte b : ba) { + sb.append(hexDigit[(b >> 4) & 0x0f]); + sb.append(hexDigit[b & 0x0f]); + } + return sb.toString(); + } + + /** + * @param ba + * @return + */ + public static String toHex(byte[] ba) { + StringBuilder sb = new StringBuilder("0x"); + for (byte b : ba) { + sb.append(hexDigit[(b >> 4) & 0x0f]); + sb.append(hexDigit[b & 0x0f]); + } + return sb.toString(); + } + + public static String toHex(byte[] ba, int start, int length) { + StringBuilder sb = new StringBuilder("0x"); + for (int i=start;i<length;++i) { + sb.append(hexDigit[(ba[i] >> 4) & 0x0f]); + sb.append(hexDigit[ba[i] & 0x0f]); + } + return sb.toString(); + } + + + public static byte[] fromHex(String s) { + if(!s.startsWith("0x")) { + return fromHexNo0x(s); + } + byte b; + int c; + byte[] ba; + int extra = s.length()%2; // odd requires extra + ba = new byte[(s.length()-2)/2 + extra]; + boolean high = extra==0; + + int idx; + for (int i=2;i<s.length();++i) { + c = s.charAt(i); + if (c>=0x30 && c<=0x39) { + b=(byte)(c-0x30); + } else if (c>=0x61 && c<=0x66) { + b=(byte)(c-0x57); // account for "A" + } else if (c>=0x41 && c<=0x46) { + b=(byte)(c-0x37); + } else { + return null; + } + idx = (i-2+extra)/2; + if (high) { + ba[idx]=(byte)(b<<4); + high = false; + } else { + ba[idx]|=b; + high = true; + } + } + return ba; + } + + /** + * Does not expect to start with "0x" + * if Any Character doesn't match, it returns null; + * + * @param s + * @return + */ + public static byte[] fromHexNo0x(String s) { + byte b; + int c; + byte[] ba; + int extra = s.length()%2; // odd requires extra byte to store + ba = new byte[(s.length())/2 + extra]; + boolean high = extra==0; + + int idx; + for (int i=0;i<s.length();++i) { + c = s.charAt(i); + if (c>=0x30 && c<=0x39) { + b=(byte)(c-0x30); + } else if (c>=0x61 && c<=0x66) { + b=(byte)(c-0x57); // account for "A" + } else if (c>=0x41 && c<=0x46) { + b=(byte)(c-0x37); + } else { + return null; + } + idx = (i+extra)/2; + if (high) { + ba[idx]=(byte)(b<<4); + high = false; + } else { + ba[idx]|=b; + high = true; + } + } + return ba; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Locator.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Locator.java new file mode 100644 index 00000000..ef3ad66c --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Locator.java @@ -0,0 +1,36 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +public interface Locator<T> { + public T get(Locator.Item item) throws LocatorException; + public boolean hasItems(); + public void invalidate(Locator.Item item) throws LocatorException; + public Locator.Item best() throws LocatorException; + public Item first() throws LocatorException; + public Item next(Item item) throws LocatorException; + public boolean refresh(); + public void destroy(); + + public interface Item {} + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/LocatorException.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/LocatorException.java new file mode 100644 index 00000000..64f07987 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/LocatorException.java @@ -0,0 +1,46 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +public class LocatorException extends Exception { + /** + * + */ + private static final long serialVersionUID = -4267929804321134469L; + + public LocatorException(String arg0) { + super(arg0); + } + + public LocatorException(Throwable arg0) { + super(arg0); + } + + public LocatorException(String arg0, Throwable arg1) { + super(arg0, arg1); + } + + public LocatorException(CharSequence cs) { + super(cs.toString()); + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Lur.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Lur.java new file mode 100644 index 00000000..0c3302f5 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Lur.java @@ -0,0 +1,91 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.security.Principal; +import java.util.List; + + + +/** + * LUR: Local User Registry + * + * Concept by Robert Garskof, Implementation by Jonathan Gathman + * + * Where we can keep local copies of users and roles for faster Authorization when asked. + * + * Note: Author cannot resist the mental image of using a Fishing Lure to this LUR pattern + * + * + */ +public interface Lur { + /** + * Allow the Lur, which has correct Permission access, to create and hand back. + */ + public Permission createPerm(String p); + + /** + * Fish for Principals in a Pond + * + * or more boringly, is the User identified within a named collection representing permission. + * + * @param principalName + * @return + */ + public boolean fish(Principal bait, Permission ... pond); + + /** + * Fish all the Principals out a Pond + * + * For additional humor, pronounce the following with a Southern Drawl, "FishOil" + * + * or more boringly, load the List with Permissions found for Principal + * + * @param principalName + * @return + */ + public void fishAll(Principal bait, List<Permission> permissions); + + /** + * Allow implementations to disconnect, or cleanup resources if unneeded + */ + public void destroy(); + + /** + * Does this LUR handle this pond exclusively? Important for EpiLUR to determine whether + * to try another (more expensive) LUR + * @param pond + * @return + */ + public boolean handlesExclusively(Permission ... pond); + + /** + * Does the LUR support a particular kind of Principal + * This can be used to check name's domain, like above, or Principal type + */ + public boolean handles(Principal principal); + + /** + * Clear: Clear any Caching, if exists + */ + public void clear(Principal p, StringBuilder report); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Permission.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Permission.java new file mode 100644 index 00000000..37050b91 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Permission.java @@ -0,0 +1,28 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +public interface Permission { + public String permType(); + public String getKey(); + public boolean match(Permission p); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/PropAccess.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/PropAccess.java new file mode 100644 index 00000000..ff9aac6f --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/PropAccess.java @@ -0,0 +1,437 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk.apps + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * + * Modifications Copyright (C) 2018 IBM. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map.Entry; +import java.util.Properties; + +import org.onap.ccsdk.apps.cadi.config.Config; +import org.onap.ccsdk.apps.cadi.config.SecurityInfo; +import org.onap.ccsdk.apps.cadi.util.Split; + +public class PropAccess implements Access { + // Sonar says cannot be static... it's ok. not too many PropAccesses created. + private final SimpleDateFormat iso8601 = newISO8601(); + private Symm symm; + public static final Level DEFAULT = Level.AUDIT; + private int level; + private Properties props; + private List<String> recursionProtection = null; + private LogIt logIt; + private String name; + + public PropAccess() { + logIt = new StreamLogIt(System.out); + init(null); + } + + /** + * This Constructor soly exists to instantiate Servlet Context Based Logging that will call "init" later. + * @param sc + */ + protected PropAccess(Object o) { + logIt = new StreamLogIt(System.out); + props = new Properties(); + } + + public PropAccess(String ... args) { + this(System.out,args); + } + + public PropAccess(PrintStream ps, String[] args) { + logIt = new StreamLogIt(ps==null?System.out:ps); + init(logIt,args); + } + + public PropAccess(LogIt logit, String[] args) { + init(logit, args); + } + + public PropAccess(Properties p) { + this(System.out,p); + } + + public PropAccess(PrintStream ps, Properties p) { + logIt = new StreamLogIt(ps==null?System.out:ps); + init(p); + } + + protected void init(final LogIt logIt, final String[] args) { + this.logIt = logIt; + Properties nprops=new Properties(); + int eq; + for (String arg : args) { + if ((eq=arg.indexOf('='))>0) { + nprops.setProperty(arg.substring(0, eq),arg.substring(eq+1)); + } + } + init(nprops); + } + + public static SimpleDateFormat newISO8601() { + return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + } + + protected synchronized void init(Properties p) { + // Make sure these two are set before any changes in Logging + name = "cadi"; + + props = new Properties(); + // First, load related System Properties + for (Entry<Object,Object> es : System.getProperties().entrySet()) { + String key = es.getKey().toString(); + for (String start : new String[] {"cadi_","aaf_","cm_"}) { + if (key.startsWith(start)) { + props.put(key, es.getValue()); + } + } + } + // Second, overlay or fill in with Passed in Props + if (p!=null) { + props.putAll(p); + } + + // Preset LogLevel + String sLevel = props.getProperty(Config.CADI_LOGLEVEL); + // Third, load any Chained Property Files + load(props.getProperty(Config.CADI_PROP_FILES)); + + if(sLevel==null) { // if LogLev wasn't set before, check again after Chained Load + sLevel = props.getProperty(Config.CADI_LOGLEVEL); + if (sLevel==null) { + level=DEFAULT.maskOf(); + } else { + level=Level.valueOf(sLevel).maskOf(); + } + } + // Setup local Symmetrical key encryption + if (symm==null) { + try { + symm = Symm.obtain(this); + } catch (CadiException e) { + System.err.append("FATAL ERROR: Cannot obtain Key Information."); + e.printStackTrace(System.err); + System.exit(1); + } + } + + name = props.getProperty(Config.CADI_LOGNAME, name); + + SecurityInfo.setHTTPProtocols(this); + + } + + + private void load(String cadi_prop_files) { + if (cadi_prop_files==null) { + return; + } + String prevKeyFile = props.getProperty(Config.CADI_KEYFILE); + + + for(String filename : Split.splitTrim(File.pathSeparatorChar, cadi_prop_files)) { + Properties fileProps = new Properties(); + File file = new File(filename); + if (file.exists()) { + printf(Level.INIT,"Loading CADI Properties from %s",file.getAbsolutePath()); + try { + FileInputStream fis = new FileInputStream(file); + try { + fileProps.load(fis); + // Only load props from recursion which are not already in props + // meaning top Property file takes precedence + for(Entry<Object, Object> es : fileProps.entrySet()) { + if(props.get(es.getKey())==null) { + String key = es.getKey().toString(); + String value = es.getValue().toString(); + props.put(key, value); + if(key.contains("pass")) { + value = "vi XX"; + } + printf(Level.DEBUG," %s=%s",key,value); + } + } + // Recursively Load + String chainProp = fileProps.getProperty(Config.CADI_PROP_FILES); + if (chainProp!=null) { + if (recursionProtection==null) { + recursionProtection = new ArrayList<>(); + recursionProtection.add(cadi_prop_files); + } + if (!recursionProtection.contains(chainProp)) { + recursionProtection.add(chainProp); + load(chainProp); // recurse + } + } + } finally { + fis.close(); + } + } catch (Exception e) { + log(e,filename,"cannot be opened"); + } + } else { + printf(Level.WARN,"Warning: recursive CADI Property %s does not exist",file.getAbsolutePath()); + } + } + + // Trim + for (Entry<Object, Object> es : props.entrySet()) { + Object value = es.getValue(); + if (value instanceof String) { + String trim = ((String)value).trim(); + // Remove Beginning/End Quotes, which might be there if mixed with Bash Props + int s = 0, e=trim.length()-1; + if (s<e && trim.charAt(s)=='"' && trim.charAt(e)=='"') { + trim=trim.substring(s+1,e); + } + if (trim!=value) { // Yes, I want OBJECT equals + props.setProperty((String)es.getKey(), trim); + } + } + } + // Reset Symm if Keyfile Changes: + String newKeyFile = props.getProperty(Config.CADI_KEYFILE); + if ((prevKeyFile!=null && newKeyFile!=null) || (newKeyFile!=null && !newKeyFile.equals(prevKeyFile))) { + try { + symm = Symm.obtain(this); + } catch (CadiException e) { + System.err.append("FATAL ERROR: Cannot obtain Key Information."); + e.printStackTrace(System.err); + System.exit(1); + } + + prevKeyFile=newKeyFile; + } + + String loglevel = props.getProperty(Config.CADI_LOGLEVEL); + if (loglevel!=null) { + try { + level=Level.valueOf(loglevel).maskOf(); + } catch (IllegalArgumentException e) { + printf(Level.ERROR,"%s=%s is an Invalid Log Level",Config.CADI_LOGLEVEL,loglevel); + } + } + } + + @Override + public void load(InputStream is) throws IOException { + props.load(is); + load(props.getProperty(Config.CADI_PROP_FILES)); + } + + @Override + public void log(Level level, Object ... elements) { + if (willLog(level)) { + logIt.push(level,elements); + } + } + + public StringBuilder buildMsg(Level level, Object[] elements) { + return buildMsg(name,iso8601,level,elements); + } + + /* + * Need to pass in DateFormat per thread, because not marked as thread safe + */ + public static StringBuilder buildMsg(final String name, final DateFormat sdf, Level level, Object[] elements) { + final StringBuilder sb; + int end = elements.length; + if(sdf==null) { + sb = new StringBuilder(); + write(true,sb,elements); + } else { + sb = new StringBuilder( + sdf.format(new Date()) + ); + sb.append(' '); + sb.append(level.name()); + sb.append(" ["); + sb.append(name); + if (end<=0) { + sb.append("] "); + } else { + int idx = 0; + if(elements[idx]!=null && + elements[idx] instanceof Integer) { + sb.append('-'); + sb.append(elements[idx]); + ++idx; + } + sb.append("] "); + write(true,sb,elements); + } + } + return sb; + } + + private static boolean write(boolean first, StringBuilder sb, Object[] elements) { + String s; + for (Object o : elements) { + if (o!=null) { + if(o.getClass().isArray()) { + first = write(first,sb,(Object[])o); + } else if(o instanceof Throwable) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + ((Throwable)o).printStackTrace(ps); + sb.append(baos.toString()); + } else { + s=o.toString(); + if (first) { + first = false; + } else { + int l = s.length(); + if (l>0) { + switch(s.charAt(l-1)) { + case ' ': + break; + default: + sb.append(' '); + } + } + } + sb.append(s); + } + } + } + return first; + } + + @Override + public void log(Exception e, Object... elements) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + pw.println(); + e.printStackTrace(pw); + log(Level.ERROR,elements,sw.toString()); + } + + @Override + public void printf(Level level, String fmt, Object... elements) { + if (willLog(level)) { + log(level,String.format(fmt, elements)); + } + } + + @Override + public void setLogLevel(Level level) { + this.level = level.maskOf(); + } + + @Override + public boolean willLog(Level level) { + return level.inMask(this.level); + } + + @Override + public ClassLoader classLoader() { + return ClassLoader.getSystemClassLoader(); + } + + @Override + public String getProperty(String tag, String def) { + return props.getProperty(tag,def); + } + + @Override + public String decrypt(String encrypted, boolean anytext) throws IOException { + return (encrypted!=null && (anytext==true || encrypted.startsWith(Symm.ENC))) + ? symm.depass(encrypted) + : encrypted; + } + + public String encrypt(String unencrypted) throws IOException { + return Symm.ENC+symm.enpass(unencrypted); + } + + ////////////////// + // Additional + ////////////////// + public String getProperty(String tag) { + return props.getProperty(tag); + } + + + public Properties getProperties() { + return props; + } + + public void setProperty(String tag, String value) { + if (value!=null) { + props.put(tag, value); + if (Config.CADI_KEYFILE.equals(tag)) { + // reset decryption too + try { + symm = Symm.obtain(this); + } catch (CadiException e) { + System.err.append("FATAL ERROR: Cannot obtain Key Information."); + e.printStackTrace(System.err); + System.exit(1); + } + } + } + } + + public interface LogIt { + public void push(Level level, Object ... elements) ; + } + + private class StreamLogIt implements LogIt { + private PrintStream ps; + + public StreamLogIt(PrintStream ps) { + this.ps = ps; + } + @Override + public void push(Level level, Object ... elements) { + ps.println(buildMsg(level,elements)); + ps.flush(); + } + } + + public void set(LogIt logit) { + logIt = logit; + } + + public void setStreamLogIt(PrintStream ps) { + logIt = new StreamLogIt(ps); + } + + public String toString() { + return props.toString(); + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Revalidator.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Revalidator.java new file mode 100644 index 00000000..94f77a46 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Revalidator.java @@ -0,0 +1,34 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +@FunctionalInterface +public interface Revalidator<TRANS> { + /** + * Re-Validate Credential + * + * @param prin + * @return + */ + public CachedPrincipal.Resp revalidate(TRANS trans, CachedPrincipal prin); + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/SecuritySetter.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/SecuritySetter.java new file mode 100644 index 00000000..01a4c82f --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/SecuritySetter.java @@ -0,0 +1,43 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + + +/** + * Apply any particular security mechanism + * + * This allows the definition of various mechanisms involved outside of DRcli jars + * + * + */ +public interface SecuritySetter<CT> { + public String getID(); + + public void setSecurity(CT client) throws CadiException; + + /** + * Returns number of bad logins registered + * @param respCode + * @return + */ + public int setLastResponse(int respCode); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/ServletContextAccess.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/ServletContextAccess.java new file mode 100644 index 00000000..cf9a90d1 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/ServletContextAccess.java @@ -0,0 +1,67 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.util.Enumeration; + +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; + +public class ServletContextAccess extends PropAccess { + + private ServletContext context; + + public ServletContextAccess(FilterConfig filterConfig) { + super(filterConfig); // protected constructor... does not have "init" called. + context = filterConfig.getServletContext(); + + for (Enumeration<?> en = filterConfig.getInitParameterNames();en.hasMoreElements();) { + String name = (String)en.nextElement(); + setProperty(name, filterConfig.getInitParameter(name)); + } + init(getProperties()); + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.PropAccess#log(org.onap.ccsdk.apps.cadi.Access.Level, java.lang.Object[]) + */ + @Override + public void log(Level level, Object... elements) { + if (willLog(level)) { + StringBuilder sb = buildMsg(level, elements); + context.log(sb.toString()); + } + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.PropAccess#log(java.lang.Exception, java.lang.Object[]) + */ + @Override + public void log(Exception e, Object... elements) { + StringBuilder sb = buildMsg(Level.ERROR, elements); + context.log(sb.toString(),e); + } + + public ServletContext context() { + return context; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Symm.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Symm.java new file mode 100644 index 00000000..48228120 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Symm.java @@ -0,0 +1,905 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Date; +import java.util.Random; + +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; + +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.config.Config; + +/** + * Key Conversion, primarily "Base64" + * + * Base64 is required for "Basic Authorization", which is an important part of the overall CADI Package. + * + * Note: This author found that there is not a "standard" library for Base64 conversion within Java. + * The source code implementations available elsewhere were surprisingly inefficient, requiring, for + * instance, multiple string creation, on a transaction pass. Integrating other packages that might be + * efficient enough would put undue Jar File Dependencies given this Framework should have none-but-Java + * dependencies. + * + * The essential algorithm is good for a symmetrical key system, as Base64 is really just + * a symmetrical key that everyone knows the values. + * + * This code is quite fast, taking about .016 ms for encrypting, decrypting and even .08 for key + * generation. The speed quality, especially of key generation makes this a candidate for a short term token + * used for identity. + * + * It may be used to easily avoid placing Clear-Text passwords in configurations, etc. and contains + * supporting functions such as 2048 keyfile generation (see keygen). This keyfile should, of course, + * be set to "400" (Unix) and protected as any other mechanism requires. + * + * AES Encryption is also employed to include standards. + * + * @author Jonathan + * + */ +public class Symm { + private static final byte[] DOUBLE_EQ = new byte[] {'=','='}; + public static final String ENC = "enc:"; + private static final Object LOCK = new Object(); + private static final SecureRandom random = new SecureRandom(); + + public final char[] codeset; + private final int splitLinesAt; + private final String encoding; + private final Convert convert; + private final boolean endEquals; + private byte[] keyBytes = null; + //Note: AES Encryption is not Thread Safe. It is Synchronized + //private AES aes = null; // only initialized from File, and only if needed for Passwords + private String name; + + /** + * This is the standard base64 Key Set. + * RFC 2045 + */ + public static final Symm base64 = new Symm( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray() + ,76, Config.UTF_8,true, "Base64"); + + public static final Symm base64noSplit = new Symm( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray() + ,Integer.MAX_VALUE, Config.UTF_8,true, "Base64, no Split"); + + /** + * This is the standard base64 set suitable for URLs and Filenames + * RFC 4648 + */ + public static final Symm base64url = new Symm( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray() + ,76, Config.UTF_8,true, "Base64 for URL"); + + /** + * A Password set, using US-ASCII + * RFC 4648 + */ + public static final Symm encrypt = new Symm(base64url.codeset,1024, "US-ASCII", false, "Base64, 1024 size"); + private static final byte[] EMPTY = new byte[0]; + + /** + * A typical set of Password Chars + * Note, this is too large to fit into the algorithm. Only use with PassGen + */ + private static char passChars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+!@#$%^&*(){}[]?:;,.".toCharArray(); + + + private static Symm internalOnly = null; + + /** + * Use this to create special case Case Sets and/or Line breaks + * + * If you don't know why you need this, use the Singleton Method + * + * @param codeset + * @param split + */ + public Symm(char[] codeset, int split, String charset, boolean useEndEquals, String name) { + this.codeset = codeset; + splitLinesAt = split; + encoding = charset; + endEquals = useEndEquals; + this.name = name; + char prev = 0, curr=0, first = 0; + int offset=Integer.SIZE; // something that's out of range for integer array + + // There can be time efficiencies gained when the underlying keyset consists mainly of ordered + // data (i.e. abcde...). Therefore, we'll quickly analyze the keyset. If it proves to have + // too much entropy, the "Unordered" algorithm, which is faster in such cases is used. + ArrayList<int[]> la = new ArrayList<>(); + for (int i=0;i<codeset.length;++i) { + curr = codeset[i]; + if (prev+1==curr) { // is next character in set + prev = curr; + } else { + if (offset!=Integer.SIZE) { // add previous range + la.add(new int[]{first,prev,offset}); + } + first = prev = curr; + offset = curr-i; + } + } + la.add(new int[]{first,curr,offset}); + if (la.size()>codeset.length/3) { + convert = new Unordered(codeset); + } else { // too random to get speed enhancement from range algorithm + int[][] range = new int[la.size()][]; + la.toArray(range); + convert = new Ordered(range); + } + } + + public Symm copy(int lines) { + return new Symm(codeset,lines,encoding,endEquals, "Copied " + lines); + } + + // Only used by keygen, which is intentionally randomized. Therefore, always use unordered + private Symm(char[] codeset, Symm parent) { + this.codeset = codeset; + splitLinesAt = parent.splitLinesAt; + endEquals = parent.endEquals; + encoding = parent.encoding; + convert = new Unordered(codeset); + } + + /** + * Obtain the base64() behavior of this class, for use in standard BASIC AUTH mechanism, etc. + * @return + */ + @Deprecated + public static final Symm base64() { + return base64; + } + + /** + * Obtain the base64() behavior of this class, for use in standard BASIC AUTH mechanism, etc. + * No Line Splitting + * @return + */ + @Deprecated + public static final Symm base64noSplit() { + return base64noSplit; + } + + /** + * Obtain the base64 "URL" behavior of this class, for use in File Names, etc. (no "/") + */ + @Deprecated + public static final Symm base64url() { + return base64url; + } + + /** + * Obtain a special ASCII version for Scripting, with base set of base64url use in File Names, etc. (no "/") + */ + public static final Symm baseCrypt() { + return encrypt; + } + + public <T> T exec(SyncExec<T> exec) throws Exception { + synchronized(LOCK) { + if (keyBytes == null) { + keyBytes = new byte[AES.AES_KEY_SIZE/8]; + int offset = (Math.abs(codeset[0])+47)%(codeset.length-keyBytes.length); + for (int i=0;i<keyBytes.length;++i) { + keyBytes[i] = (byte)codeset[i+offset]; + } + } + } + return exec.exec(new AES(keyBytes,0,keyBytes.length)); + } + + public interface Encryption { + public CipherOutputStream outputStream(OutputStream os, boolean encrypt); + public CipherInputStream inputStream(InputStream is, boolean encrypt); + } + + public static interface SyncExec<T> { + public T exec(Encryption enc) throws IOException, Exception; + } + + public byte[] encode(byte[] toEncrypt) throws IOException { + if (toEncrypt==null) { + return EMPTY; + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(toEncrypt.length*1.25)); + encode(new ByteArrayInputStream(toEncrypt),baos); + return baos.toByteArray(); + } + } + + public byte[] decode(byte[] encrypted) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(encrypted.length*1.25)); + decode(new ByteArrayInputStream(encrypted),baos); + return baos.toByteArray(); + } + + /** + * Helper function for String API of "Encode" + * use "getBytes" with appropriate char encoding, etc. + * + * @param str + * @return + * @throws IOException + */ + public String encode(String str) throws IOException { + byte[] array; + boolean useDefaultEncoding = false; + try { + array = str.getBytes(encoding); + } catch (IOException e) { + array = str.getBytes(); // take default + useDefaultEncoding = true; + } + // Calculate expected size to avoid any buffer expansion copies within the ByteArrayOutput code + ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(array.length*1.363)); // account for 4 bytes for 3 and a byte or two more + + encode(new ByteArrayInputStream(array),baos); + if (useDefaultEncoding) { + return baos.toString(); + } + return baos.toString(encoding); + } + + /** + * Helper function for the String API of "Decode" + * use "getBytes" with appropriate char encoding, etc. + * @param str + * @return + * @throws IOException + */ + public String decode(String str) throws IOException { + byte[] array; + boolean useDefaultEncoding = false; + try { + array = str.getBytes(encoding); + } catch (IOException e) { + array = str.getBytes(); // take default + useDefaultEncoding = true; + } + // Calculate expected size to avoid any buffer expansion copies within the ByteArrayOutput code + ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(array.length*.76)); // Decoding is 3 bytes for 4. Allocate slightly more than 3/4s + decode(new ByteArrayInputStream(array), baos); + if (useDefaultEncoding) { + return baos.toString(); + } + return baos.toString(encoding); + } + + /** + * Convenience Function + * + * encode String into InputStream and call encode(InputStream, OutputStream) + * + * @param string + * @param out + * @throws IOException + */ + public void encode(String string, OutputStream out) throws IOException { + encode(new ByteArrayInputStream(string.getBytes()),out); + } + + /** + * Convenience Function + * + * encode String into InputStream and call decode(InputStream, OutputStream) + * + * @param string + * @param out + * @throws IOException + */ + public void decode(String string, OutputStream out) throws IOException { + decode(new ByteArrayInputStream(string.getBytes()),out); + } + + public void encode(InputStream is, OutputStream os, byte[] prefix) throws IOException { + os.write(prefix); + encode(is,os); + } + + /** + * encode InputStream onto Output Stream + * + * @param is + * @param estimate + * @return + * @throws IOException + */ + public void encode(InputStream is, OutputStream os) throws IOException { + // StringBuilder sb = new StringBuilder((int)(estimate*1.255)); // try to get the right size of StringBuilder from start.. slightly more than 1.25 times + int prev=0; + int read, idx=0, line=0; + boolean go; + do { + read = is.read(); + if (go = read>=0) { + if (line>=splitLinesAt) { + os.write('\n'); + line = 0; + } + switch(++idx) { // 1 based reading, slightly faster ++ + case 1: // ptr is the first 6 bits of read + os.write(codeset[read>>2]); + prev = read; + break; + case 2: // ptr is the last 2 bits of prev followed by the first 4 bits of read + os.write(codeset[((prev & 0x03)<<4) | (read>>4)]); + prev = read; + break; + default: //(3+) + // Char 1 is last 4 bits of prev plus the first 2 bits of read + // Char 2 is the last 6 bits of read + os.write(codeset[(((prev & 0xF)<<2) | (read>>6))]); + if (line==splitLinesAt) { // deal with line splitting for two characters + os.write('\n'); + line=0; + } + os.write(codeset[(read & 0x3F)]); + ++line; + idx = 0; + prev = 0; + } + ++line; + } else { // deal with any remaining bits from Prev, then pad + switch(idx) { + case 1: // just the last 2 bits of prev + os.write(codeset[(prev & 0x03)<<4]); + if (endEquals)os.write(DOUBLE_EQ); + break; + case 2: // just the last 4 bits of prev + os.write(codeset[(prev & 0xF)<<2]); + if (endEquals)os.write('='); + break; + } + idx = 0; + } + + } while (go); + } + + public void decode(InputStream is, OutputStream os, int skip) throws IOException { + if (is.skip(skip)!=skip) { + throw new IOException("Error skipping on IOStream in Symm"); + } + decode(is,os); + } + + /** + * Decode InputStream onto OutputStream + * @param is + * @param os + * @throws IOException + */ + public void decode(InputStream is, OutputStream os) throws IOException { + int read, idx=0; + int prev=0, index; + while ((read = is.read())>=0) { + index = convert.convert(read); + if (index>=0) { + switch(++idx) { // 1 based cases, slightly faster ++ + case 1: // index goes into first 6 bits of prev + prev = index<<2; + break; + case 2: // write second 2 bits of into prev, write byte, last 4 bits go into prev + os.write((byte)(prev|(index>>4))); + prev = index<<4; + break; + case 3: // first 4 bits of index goes into prev, write byte, last 2 bits go into prev + os.write((byte)(prev|(index>>2))); + prev = index<<6; + break; + default: // (3+) | prev and last six of index + os.write((byte)(prev|(index&0x3F))); + idx = prev = 0; + } + } + }; + os.flush(); + } + + /** + * Interface to allow this class to choose which algorithm to find index of character in Key + * @author Jonathan + * + */ + private interface Convert { + public int convert(int read) throws IOException; + } + + /** + * Ordered uses a range of orders to compare against, rather than requiring the investigation + * of every character needed. + * @author Jonathan + * + */ + private static final class Ordered implements Convert { + private int[][] range; + public Ordered(int[][] range) { + this.range = range; + } + public int convert(int read) throws IOException { + // System.out.print((char)read); + switch(read) { + case -1: + case '=': + case ' ': + case '\n': + case '\r': + return -1; + } + for (int i=0;i<range.length;++i) { + if (read >= range[i][0] && read<=range[i][1]) { + return read-range[i][2]; + } + } + throw new IOException("Unacceptable Character in Stream"); + } + } + + /** + * Unordered, i.e. the key is purposely randomized, simply has to investigate each character + * until we find a match. + * @author Jonathan + * + */ + private static final class Unordered implements Convert { + private char[] codec; + public Unordered(char[] codec) { + this.codec = codec; + } + public int convert(int read) throws IOException { + switch(read) { + case -1: + case '=': + case '\n': + case '\r': + return -1; + } + for (int i=0;i<codec.length;++i) { + if (codec[i]==read)return i; + } + // don't give clue in Encryption mode + throw new IOException("Unacceptable Character in Stream"); + } + } + + /** + * Generate a 2048 based Key from which we extract our code base + * + * @return + * @throws IOException + */ + public static byte[] keygen() throws IOException { + byte inkey[] = new byte[0x600]; + new SecureRandom().nextBytes(inkey); + ByteArrayOutputStream baos = new ByteArrayOutputStream(0x800); + base64url.encode(new ByteArrayInputStream(inkey), baos); + return baos.toByteArray(); + } + + // A class allowing us to be less predictable about significant digits (i.e. not picking them up from the + // beginning, and not picking them up in an ordered row. Gives a nice 2048 with no visible patterns. + private class Obtain { + private int last; + private int skip; + private int length; + private byte[] key; + + private Obtain(Symm b64, byte[] key) { + skip = Math.abs(key[key.length-13]%key.length); + if ((key.length&0x1) == (skip&0x1)) { // if both are odd or both are even + ++skip; + } + length = b64.codeset.length; + last = 17+length%59; // never start at beginning + this.key = key; + } + + private int next() { + return Math.abs(key[(++last*skip)%key.length])%length; + } + }; + + /** + * Obtain a Symm from "keyfile" (Config.KEYFILE) property + * + * @param acesss + * @return + * @throws IOException + * @throws CadiException + */ + public static Symm obtain(Access access) throws CadiException { + String keyfile = access.getProperty(Config.CADI_KEYFILE,null); + if (keyfile!=null) { + Symm symm = Symm.baseCrypt(); + + File file = new File(keyfile); + try { + access.log(Level.INIT, Config.CADI_KEYFILE,"points to",file.getCanonicalPath()); + } catch (IOException e1) { + access.log(Level.INIT, Config.CADI_KEYFILE,"points to",file.getAbsolutePath()); + } + if (file.exists()) { + try { + FileInputStream fis = new FileInputStream(file); + try { + symm = Symm.obtain(fis); + } finally { + try { + fis.close(); + } catch (IOException e) { + } + } + } catch (IOException e) { + access.log(e, "Cannot load keyfile"); + } + } else { + String filename; + try { + filename = file.getCanonicalPath(); + } catch (IOException e) { + filename = file.getAbsolutePath(); + } + throw new CadiException("ERROR: " + filename + " does not exist!"); + } + return symm; + } else { + try { + return internalOnly(); + } catch (IOException e) { + throw new CadiException(e); + } + } + } + /** + * Create a new random key + */ + public Symm obtain() throws IOException { + byte inkey[] = new byte[0x800]; + new SecureRandom().nextBytes(inkey); + Symm s = obtain(inkey); + s.name = "from Random"; + return s; + } + + /** + * Obtain a Symm from 2048 key from a String + * + * @param key + * @return + * @throws IOException + */ + public static Symm obtain(String key) throws IOException { + Symm s = obtain(new ByteArrayInputStream(key.getBytes())); + s.name = "from String"; + return s; + } + + /** + * Obtain a Symm from 2048 key from a Stream + * + * @param is + * @return + * @throws IOException + */ + public static Symm obtain(InputStream is) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + base64url.decode(is, baos); + } catch (IOException e) { + // don't give clue + throw new IOException("Invalid Key"); + } + byte[] bkey = baos.toByteArray(); + if (bkey.length<0x88) { // 2048 bit key + throw new IOException("Invalid key"); + } + Symm s = baseCrypt().obtain(bkey); + s.name = "from InputStream"; + return s; + } + + /** + * Convenience for picking up Keyfile + * + * @param f + * @return + * @throws IOException + */ + public static Symm obtain(File f) throws IOException { + FileInputStream fis = new FileInputStream(f); + try { + Symm s = obtain(fis); + s.name = "From " + f.getCanonicalPath() + " dated " + new Date(f.lastModified()); + return s; + } finally { + fis.close(); + } + } + /** + * Decrypt into a String + * + * Convenience method + * + * @param password + * @return + * @throws IOException + */ + public String enpass(String password) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + enpass(password,baos); + return new String(baos.toByteArray()); + } + + /** + * Create an encrypted password, making sure that even short passwords have a minimum length. + * + * @param password + * @param os + * @throws IOException + */ + public void enpass(final String password, final OutputStream os) throws IOException { + if (password==null) { + throw new IOException("Invalid password passed"); + } + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + byte[] bytes = password.getBytes(); + if (this.getClass().getSimpleName().startsWith("base64")) { // don't expose randomization + dos.write(bytes); + } else { + + Random r = new SecureRandom(); + int start = 0; + byte b; + for (int i=0;i<3;++i) { + dos.writeByte(b=(byte)r.nextInt()); + start+=Math.abs(b); + } + start%=0x7; + for (int i=0;i<start;++i) { + dos.writeByte(r.nextInt()); + } + dos.writeInt((int)System.currentTimeMillis()); + int minlength = Math.min(0x9,bytes.length); + dos.writeByte(minlength); // expect truncation + if (bytes.length<0x9) { + for (int i=0;i<bytes.length;++i) { + dos.writeByte(r.nextInt()); + dos.writeByte(bytes[i]); + } + // make sure it's long enough + for (int i=bytes.length;i<0x9;++i) { + dos.writeByte(r.nextInt()); + } + } else { + dos.write(bytes); + } + } + + // 7/21/2016 Jonathan add AES Encryption to the mix + try { + exec(new SyncExec<Void>() { + @Override + public Void exec(Encryption enc) throws Exception { + CipherInputStream cis = enc.inputStream(new ByteArrayInputStream(baos.toByteArray()), true); + try { + encode(cis,os); + } finally { + os.flush(); + cis.close(); + } + return null; + } + }); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException(e); + } + } + + /** + * Decrypt a password into a String + * + * Convenience method + * + * @param password + * @return + * @throws IOException + */ + public String depass(String password) throws IOException { + if (password==null)return null; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + depass(password,baos); + return new String(baos.toByteArray()); + } + + /** + * Decrypt a password + * + * Skip Symm.ENC + * + * @param password + * @param os + * @return + * @throws IOException + */ + public long depass(final String password, final OutputStream os) throws IOException { + int offset = password.startsWith(ENC)?4:0; + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final ByteArrayInputStream bais = new ByteArrayInputStream(password.getBytes(),offset,password.length()-offset); + try { + exec(new SyncExec<Void>() { + @Override + public Void exec(Encryption enc) throws IOException { + CipherOutputStream cos = enc.outputStream(baos, false); + decode(bais,cos); + cos.close(); // flush + return null; + } + }); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException(e); + } + + byte[] bytes = baos.toByteArray(); + DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bytes)); + long time; + if (this.getClass().getSimpleName().startsWith("base64")) { // don't expose randomization + os.write(bytes); + time = 0L; + } else { + int start=0; + for (int i=0;i<3;++i) { + start+=Math.abs(dis.readByte()); + } + start%=0x7; + for (int i=0;i<start;++i) { + dis.readByte(); + } + time = (dis.readInt() & 0xFFFF)|(System.currentTimeMillis()&0xFFFF0000); + int minlength = dis.readByte(); + if (minlength<0x9){ + DataOutputStream dos = new DataOutputStream(os); + for (int i=0;i<minlength;++i) { + dis.readByte(); + dos.writeByte(dis.readByte()); + } + } else { + int pre =((Byte.SIZE*3+Integer.SIZE+Byte.SIZE)/Byte.SIZE)+start; + os.write(bytes, pre, bytes.length-pre); + } + } + return time; + } + + public static String randomGen(int numBytes) { + return randomGen(passChars,numBytes); + } + + public static String randomGen(char[] chars ,int numBytes) { + int rint; + StringBuilder sb = new StringBuilder(numBytes); + for (int i=0;i<numBytes;++i) { + rint = random.nextInt(chars.length); + sb.append(chars[rint]); + } + return sb.toString(); + } + // Internal mechanism for helping to randomize placement of characters within a Symm codeset + // Based on an incoming data stream (originally created randomly, but can be recreated within + // 2048 key), go after a particular place in the new codeset. If that codeset spot is used, then move + // right or left (depending on iteration) to find the next available slot. In this way, key generation + // is speeded up by only enacting N iterations, but adds a spreading effect of the random number stream, so that keyset is also + // shuffled for a good spread. It is, however, repeatable, given the same number set, allowing for + // quick recreation when the official stream is actually obtained. + public Symm obtain(byte[] key) throws IOException { + int filled = codeset.length; + char[] seq = new char[filled]; + int end = filled--; + + boolean right = true; + int index; + Obtain o = new Obtain(this,key); + + while (filled>=0) { + index = o.next(); + if (index<0 || index>=codeset.length) { + System.out.println("uh, oh"); + } + if (right) { // alternate going left or right to find the next open slot (keeps it from taking too long to hit something) + for (int j=index;j<end;++j) { + if (seq[j]==0) { + seq[j]=codeset[filled]; + --filled; + break; + } + } + right = false; + } else { + for (int j=index;j>=0;--j) { + if (seq[j]==0) { + seq[j]=codeset[filled]; + --filled; + break; + } + } + right = true; + } + } + Symm newSymm = new Symm(seq,this); + newSymm.name = "from bytes"; + // Set the KeyBytes + try { + newSymm.keyBytes = new byte[AES.AES_KEY_SIZE/8]; + int offset = (Math.abs(key[(47%key.length)])+137)%(key.length-newSymm.keyBytes.length); + for (int i=0;i<newSymm.keyBytes.length;++i) { + newSymm.keyBytes[i] = key[i+offset]; + } + } catch (Exception e) { + throw new IOException(e); + } + + return newSymm; + } + + /** + * This Symm is generated for internal JVM use. It has no external keyfile, but can be used + * for securing Memory, as it remains the same ONLY of the current JVM + * @return + * @throws IOException + */ + public static synchronized Symm internalOnly() throws IOException { + if (internalOnly==null) { + ByteArrayInputStream baos = new ByteArrayInputStream(keygen()); + try { + internalOnly = Symm.obtain(baos); + } finally { + baos.close(); + } + } + return internalOnly; + } + + @Override + public String toString() { + return name; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Taf.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Taf.java new file mode 100644 index 00000000..c273e68f --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Taf.java @@ -0,0 +1,57 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import org.onap.ccsdk.apps.cadi.taf.TafResp; + + +/** + * TAF - Transmutative Assertion Framework. + * + * This main Interface embodies the essential of the assertion, where a number of different TAFs might be used to authenticate + * and that authentication to be recognized through other elements. + * + * Concept by Robert Garskof. Implemented by Jonathan Gathman + * + * @author Jonathan + * + */ +public interface Taf { + enum LifeForm {CBLF, SBLF, LFN}; + /** + * The lifeForm param is a humorous way of describing whether the interaction is proceeding from direct Human Interaction via a browser + * or App which can directly query a memorized password, key sequence, bio-feedback, from that user, or a machine mechanism for which identity + * can more easily be determined by Certificate, Mechanical ID/Password etc. Popularized in modern culture and Science Fiction (especially + * Star Trek), we (starting with Robert Garskof) use the terms "Carbon Based Life Form" (CBLF) for mechanisms with people at the end of them, or + * "Silicon Based Life Forms" (SBLF) to indicate machine only interactions. I have added "LFN" for (Life-Form Neutral) to aid identifying + * processes for which it doesn't matter whether there is a human at the immediate end of the chain, or cannot be determined mechanically. + * + * The variable parameter is not necessarily ideal, but with too many unknown Tafs to be created, flexibility, + * is unfortunately required at this point. Future versions could lock this down more. Jonathan 10/18/2012 + * + * @param lifeForm + * @param info + * @return + */ + public TafResp validate(LifeForm reading, String ... info); + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Transmutate.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Transmutate.java new file mode 100644 index 00000000..1e8f7c69 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/Transmutate.java @@ -0,0 +1,45 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.security.Principal; + +/** + * The unique element of TAF is that we establish the relationship/mechanism to mutate the Principal derived from + * one Authentication mechanism into a trustable Principal of another. The mechanism needs to be decided by system + * trusting. + * + * The Generic "T" is used so that the code used will be very specific for the implementation, enforced by Compiler + * + * This interface will allow differences of trusting Transmutation of Authentication + * @author Jonathan + * + */ +public interface Transmutate<T> { + /** + * Mutate the (assumed validated) Principal into the expected Principal name to be used to construct + * + * @param p + * @return + */ + public T mutate(Principal p); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/TrustChecker.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/TrustChecker.java new file mode 100644 index 00000000..8fae0642 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/TrustChecker.java @@ -0,0 +1,52 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import jakarta.servlet.http.HttpServletRequest; + +import org.onap.ccsdk.apps.cadi.taf.TafResp; + +/** + * Change to another Principal based on Trust of caller and User Chain (if desired) + * + * @author Jonathan + * + */ +public interface TrustChecker { + public TafResp mayTrust(TafResp tresp, HttpServletRequest req); + + /** + * A class that trusts no-one else, so just return same TResp + */ + public static TrustChecker NOTRUST = new TrustChecker() { + @Override + public TafResp mayTrust(TafResp tresp, HttpServletRequest req) { + return tresp; + } + + @Override + public void setLur(Lur lur) { + } + }; + + public void setLur(Lur lur); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/User.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/User.java new file mode 100644 index 00000000..d4aa3f9e --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/User.java @@ -0,0 +1,176 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +import java.security.Principal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.onap.ccsdk.apps.cadi.lur.LocalPermission; + +/** + * Class to hold info from the User Perspective. + * + * + */ +public final class User<PERM extends Permission> { + private static final Map<String,Permission> NULL_MAP = new HashMap<>(); + public String name; + private byte[] cred; + public Principal principal; + Map<String, Permission> perms; + long permExpires; + private final long interval; + int count; + + // Note: This should only be used for Local RBAC (in memory) + public User(Principal principal) { + this.principal = principal; + name = principal.getName(); + perms = NULL_MAP; + permExpires = Long.MAX_VALUE; // Never. Well, until 64 bits of millis since 1970 expires... + interval = 0L; + count = 0; + } + + public User(String name, byte[] cred) { + this.principal = null; + this.name = name; + this.cred = cred; + perms = NULL_MAP; + permExpires = Long.MAX_VALUE; // Never. Well, until 64 bits of millis since 1970 expires... + interval = 0L; + count = 0; + } + + public User(Principal principal, long expireInterval) { + this.principal = principal; + this.name = principal.getName(); + perms = NULL_MAP; + expireInterval = Math.max(expireInterval, 0); // avoid < 1 + interval = Math.max(AbsUserCache.MIN_INTERVAL,Math.min(expireInterval,AbsUserCache.MAX_INTERVAL)); + count = 0; + renewPerm(); + renewPerm(); + } + + public User(String name, byte[] cred, long expireInterval) { + this.principal = null; + this.name = name; + this.cred = cred; + perms = NULL_MAP; + expireInterval = Math.max(expireInterval, 0); // avoid < 1 + interval = Math.max(AbsUserCache.MIN_INTERVAL,Math.min(expireInterval,AbsUserCache.MAX_INTERVAL)); + count = 0; + renewPerm(); + } + + public void renewPerm() { + permExpires = System.currentTimeMillis()+interval; + } + + public long permExpires() { + return permExpires; + } + + public boolean permExpired() { + return System.currentTimeMillis() > permExpires; + } + + public boolean noPerms() { + return perms==null || perms==NULL_MAP || perms.values().size()==0; + } + + public synchronized void setNoPerms() { + perms=NULL_MAP; + renewPerm(); + } + + public boolean permsUnloaded() { + return perms==null || perms==NULL_MAP; + } + + public synchronized void incCount() { + ++count; + } + + public synchronized void resetCount() { + count=0; + } + + public Map<String,Permission> newMap() { + return new ConcurrentHashMap<>(); + } + + public void add(LocalPermission permission) { + if (perms==NULL_MAP) { + perms=newMap(); + } + perms.put(permission.getKey(),permission); + } + + public void add(Map<String, Permission> newMap, PERM permission) { + newMap.put(permission.getKey(),permission); + } + + public synchronized void setMap(Map<String, Permission> newMap) { + perms = newMap; + renewPerm(); + } + + public boolean contains(Permission perm) { + for (Permission p : perms.values()) { + if (p.match(perm)) return true; + } + return false; + } + + public void copyPermsTo(List<Permission> sink) { + sink.addAll(perms.values()); + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(principal.getName()); + sb.append('|'); + boolean first = true; + synchronized(perms) { + for (Permission gp : perms.values()) { + if (first) { + first = false; + sb.append(':'); + } else { + sb.append(','); + } + sb.append(gp.getKey()); + } + } + return sb.toString(); + } + + public byte[] getCred() { + return cred; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/UserChain.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/UserChain.java new file mode 100644 index 00000000..1e1fc322 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/UserChain.java @@ -0,0 +1,42 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi; + +/** + * Interface to add a User Chain String to Principal + * + * + * + * Where + * APP is name suitable for Logging (i.e. official App Acronym) + * ID is official User or MechID, best if includes Identity Source (i.e. ab1234@people.osaaf.org) + * Protocol is the Security protocol, + * + * Format:<ID>:<APP>:<protocol>[:AS][,<ID>:<APP>:<protocol>]* + * + * + * + */ +public interface UserChain { + public enum Protocol {BasicAuth,Cookie,Cert,OAuth}; + public String userChain(); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/Config.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/Config.java new file mode 100644 index 00000000..b50bfc2c --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/Config.java @@ -0,0 +1,1078 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.config; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.onap.ccsdk.apps.cadi.AbsUserCache; +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.CachingLur; +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.Connector; +import org.onap.ccsdk.apps.cadi.CredVal; +import org.onap.ccsdk.apps.cadi.CredValDomain; +import org.onap.ccsdk.apps.cadi.Locator; +import org.onap.ccsdk.apps.cadi.LocatorException; +import org.onap.ccsdk.apps.cadi.Lur; +import org.onap.ccsdk.apps.cadi.PropAccess; +import org.onap.ccsdk.apps.cadi.Symm; +import org.onap.ccsdk.apps.cadi.TrustChecker; +import org.onap.ccsdk.apps.cadi.lur.EpiLur; +import org.onap.ccsdk.apps.cadi.lur.LocalLur; +import org.onap.ccsdk.apps.cadi.lur.NullLur; +import org.onap.ccsdk.apps.cadi.taf.HttpEpiTaf; +import org.onap.ccsdk.apps.cadi.taf.HttpTaf; +import org.onap.ccsdk.apps.cadi.taf.basic.BasicHttpTaf; +import org.onap.ccsdk.apps.cadi.taf.cert.X509Taf; +import org.onap.ccsdk.apps.cadi.taf.dos.DenialOfServiceTaf; +import org.onap.ccsdk.apps.cadi.util.FixURIinfo; +import org.onap.ccsdk.apps.cadi.util.Split; + +/** + * Create a Consistent Configuration mechanism, even when configuration styles + * are as vastly different as Properties vs JavaBeans vs FilterConfigs... + * + * @author Jonathan + * + */ +public class Config { + + private static final String AAF_V2_0 = "org.onap.aaf.cadi.aaf.v2_0"; + private static final String AAF_V2_0_AAFCON = AAF_V2_0 + ".AAFCon"; + private static final String AAF_V2_0_AAF_LUR_PERM = AAF_V2_0 + ".AAFLurPerm"; + public static final String AAF_V2_0_AAF_CON_HTTP = AAF_V2_0 + ".AAFConHttp"; + + private static final String OAUTH = "org.onap.auth.oauth"; + private static final String OAUTH_TOKEN_MGR = OAUTH + ".TokenMgr"; + private static final String OAUTH_HTTP_TAF = OAUTH + ".OAuth2HttpTaf"; + private static final String OAUTH_DIRECT_TAF = OAUTH + ".OAuthDirectTAF"; + public static final String UTF_8 = "UTF-8"; + + // Property Names associated with configurations. + // As of 1.0.2, these have had the dots removed so as to be compatible with + // JavaBean style + // configurations as well as property list style. + public static final String HOSTNAME = "hostname"; + public static final String CADI_PROP_FILES = "cadi_prop_files"; // Additional Properties files (separate with ;) + public static final String CADI_LOGLEVEL = "cadi_loglevel"; + public static final String CADI_LOGDIR = "cadi_log_dir"; + public static final String CADI_ETCDIR = "cadi_etc_dir"; + public static final String CADI_LOGNAME = "cadi_logname"; + // public static final String CADI_LOGFMT="cad_logging_format"; + // public static final String CADI_LOGFMT_UTC="UTC"; + // public static final String CADI_LOGFMT_ISO8601="ISO-8601"; + public static final String CADI_KEYFILE = "cadi_keyfile"; + public static final String CADI_KEYSTORE = "cadi_keystore"; + public static final String CADI_KEYSTORE_PASSWORD = "cadi_keystore_password"; + public static final String CADI_ALIAS = "cadi_alias"; + public static final String CADI_CLIENT_ALIAS = "cadi_client_alias"; + public static final String CADI_LOGINPAGE_URL = "cadi_loginpage_url"; + public static final String CADI_LATITUDE = "cadi_latitude"; + public static final String CADI_LONGITUDE = "cadi_longitude"; + + public static final String CADI_KEY_PASSWORD = "cadi_key_password"; + public static final String CADI_TRUSTSTORE = "cadi_truststore"; + public static final String CADI_TRUSTSTORE_PASSWORD = "cadi_truststore_password"; + public static final String CADI_X509_ISSUERS = "cadi_x509_issuers"; + public static final String CADI_TRUST_MASKS = "cadi_trust_masks"; + public static final String CADI_TRUST_PERM = "cadi_trust_perm"; // IDs with this perm can utilize the "AS " user + // concept + public static final String CADI_PROTOCOLS = "cadi_protocols"; + public static final String CADI_NOAUTHN = "cadi_noauthn"; + public static final String CADI_LOC_LIST = "cadi_loc_list"; + + // Special Behaviors + public static final String CADI_BATH_CONVERT = "cadi_bath_convert"; + public static final String CADI_API_ENFORCEMENT = "cadi_api_enforcement"; + public static final String CADI_ADD_TAFS = "cadi_add_tafs"; + public static final String CADI_ADD_LURS = "cadi_add_lurs"; + + public static final String CADI_USER_CHAIN_TAG = "cadi_user_chain"; + public static final String CADI_USER_CHAIN = "USER_CHAIN"; + + public static final String CADI_OAUTH2_URL = "cadi_oauth2_url"; + public static final String CADI_TOKEN_DIR = "cadi_token_dir"; + + public static final String HTTPS_PROTOCOLS = "https.protocols"; + public static final String HTTPS_CLIENT_PROTOCOLS = "jdk.tls.client.protocols"; + public static final String HTTPS_PROTOCOLS_DEFAULT = "TLSv1.1,TLSv1.2"; + public static final String HTTPS_CIPHER_SUITES = "https.cipherSuites"; + public static final String HTTPS_CIPHER_SUITES_DEFAULT = "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA," + + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_128_CBC_SHA,TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA," + + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_DSS_WITH_AES_128_CBC_SHA," + + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,TLS_ECDHE_RSA_WITH_RC4_128_SHA,TLS_ECDH_ECDSA_WITH_RC4_128_SHA," + + "TLS_ECDH_RSA_WITH_RC4_128_SHA,TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA,TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA," + + "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA,TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA,TLS_EMPTY_RENEGOTIATION_INFO_SCSV"; + + public static final String LOCALHOST_ALLOW = "localhost_allow"; + public static final String LOCALHOST_DENY = "localhost_deny"; + + public static final String BASIC_REALM = "basic_realm"; // what is sent to the client + public static final String BASIC_WARN = "basic_warn"; // Warning of insecure channel + public static final String USERS = "local_users"; + public static final String GROUPS = "local_groups"; + public static final String WRITE_TO = "local_writeto"; // dump RBAC to local file in Tomcat Style (some apps use) + + public static final String OAUTH_CLIENT_ID = "client_id"; + public static final String OAUTH_CLIENT_SECRET = "client_secret"; + + public static final String AAF_ENV = "aaf_env"; + public static final String AAF_ROOT_NS = "aaf_root_ns"; + public static final String AAF_ROOT_NS_DEF = "org.osaaf.aaf"; + public static final String AAF_ROOT_COMPANY = "aaf_root_company"; + /** + * Use Config.getAAFLocateUrl(access) to get correct property in/out of + * container + */ + public static final String AAF_LOCATE_URL = "aaf_locate_url"; // URL for AAF locator + public static final String AAF_LOCATE_URL_TAG = "AAF_LOCATE_URL"; // Name of Above for use in Config Variables. + public static final String AAF_DEFAULT_API_VERSION = "2.1"; + public static final String AAF_DEPLOYED_VERSION = "aaf_deployed_version"; + public static final String AAF_API_VERSION = "aaf_api_version"; + public static final String AAF_URL = "aaf_url"; // URL for AAF... Use to trigger AAF configuration + public static final String AAF_LOCATOR_CLASS = "aaf_locator_class"; + // AAF Locator Entries are ADDITIONAL entries, which also gives the Property + // ability + // to set these entries manually + // example: adding a K8S name like "oom" + // this will allow Registrations to pick up + // locator_ns.oom for onap's "OOM" based k8s entries, etc. + public static final String AAF_LOCATOR_CONTAINER = "aaf_locator_container"; + // An ID for another Container, to be used to avoid picking up the wrong + // internal info + // for another container. + public static final String AAF_LOCATOR_CONTAINER_ID = "aaf_locator_container_id"; + public static final String AAF_LOCATOR_CONTAINER_NS = "aaf_locator_container_ns"; + public static final String AAF_LOCATOR_VERSION = "aaf_locator_version"; + public static final String AAF_LOCATOR_PROTOCOL = "aaf_locator_protocol"; + public static final String AAF_LOCATOR_SUBPROTOCOL = "aaf_locator_subprotocol"; + public static final String AAF_LOCATOR_APP_NS = "aaf_locator_app_ns"; + public static final String AAF_LOCATOR_ENTRIES = "aaf_locator_entries"; + public static final String AAF_LOCATOR_FQDN = "aaf_locator_fqdn"; + public static final String AAF_LOCATOR_NAME = "aaf_locator_name"; + public static final String AAF_LOCATOR_PUBLIC_PORT = "aaf_locator_public_port"; + public static final String AAF_LOCATOR_PUBLIC_FQDN = "aaf_locator_public_fqdn"; + public static final String AAF_LOCATOR_PUBLIC_NAME = "aaf_locator_public_name"; + + // AAF Service will write to the Audit Log if a past due AAF stored Password + // is being used within # of days specified. + public static final String AAF_CRED_WARN_DAYS = "aaf_cred_warn_days"; + public static final String AAF_CRED_WARN_DAYS_DFT = "7"; + + public static final String AAF_APPID = "aaf_id"; + public static final String AAF_APPPASS = "aaf_password"; + public static final String AAF_LUR_CLASS = "aaf_lur_class"; + public static final String AAF_TAF_CLASS = "aaf_taf_class"; + public static final String AAF_CONNECTOR_CLASS = "aaf_connector_class"; + public static final String AAF_CONN_TIMEOUT = "aaf_conn_timeout"; + public static final String AAF_CONN_TIMEOUT_DEF = "3000"; + public static final String AAF_CONN_IDLE_TIMEOUT = "aaf_conn_idle_timeout"; // only for Direct Jetty Access. + public static final String AAF_CONN_IDLE_TIMEOUT_DEF = "10000"; // only for Direct Jetty Access. + + // Default Classes: These are for Class loading to avoid direct compile links + public static final String AAF_TAF_CLASS_DEF = "org.onap.aaf.cadi.aaf.v2_0.AAFTaf"; + public static final String AAF_LOCATOR_CLASS_DEF = "org.onap.aaf.cadi.aaf.v2_0.AAFLocator"; + public static final String AAF_LOCATOR_CLASS_SINGLE = "org.onap.aaf.cadi.locator.SingleEndpointLocator"; + + public static final String CADI_OLUR_CLASS_DEF = "org.onap.aaf.cadi.olur.OLur"; + public static final String CADI_OBASIC_HTTP_TAF_DEF = "org.onap.aaf.cadi.obasic.OBasicHttpTaf"; + public static final String CADI_AAF_CON_DEF = "org.onap.aaf.cadi.aaf.v2_0.AAFCon"; + + public static final String AAF_CALL_TIMEOUT = "aaf_timeout"; + public static final String AAF_CALL_TIMEOUT_DEF = "5000"; + public static final String AAF_USER_EXPIRES = "aaf_user_expires"; + public static final String AAF_USER_EXPIRES_DEF = "600000"; // Default is 10 mins + public static final String AAF_CLEAN_INTERVAL = "aaf_clean_interval"; + public static final String AAF_CLEAN_INTERVAL_DEF = "30000"; // Default is 30 seconds + public static final String AAF_REFRESH_TRIGGER_COUNT = "aaf_refresh_trigger_count"; + public static final String AAF_REFRESH_TRIGGER_COUNT_DEF = "3"; // Default is 10 mins + + public static final String AAF_HIGH_COUNT = "aaf_high_count"; + public static final String AAF_HIGH_COUNT_DEF = "1000"; // Default is 1000 entries + public static final String AAF_PERM_MAP = "aaf_perm_map"; + // public static final String AAF_COMPONENT = "aaf_component"; + public static final String AAF_CERT_IDS = "aaf_cert_ids"; + public static final String AAF_DEBUG_IDS = "aaf_debug_ids"; // comma delimited + public static final String AAF_DATA_DIR = "aaf_data_dir"; // AAF processes and Components only. + + public static final String AAF_URL_OAUTH = "aaf_url_oauth"; + public static final String AAF_URL_GUI = "aaf_url_gui"; + public static final String AAF_URL_FS = "aaf_url_fs"; + public static final String AAF_URL_CM = "aaf_url_cm"; + public static final String AAF_URL_CM_DEF = "https://AAF_LOCATE_URL/AAF_NS.cm:" + AAF_DEFAULT_API_VERSION; + public static final String AAF_URL_HELLO = "aaf_url_hello"; + public static final String CM_TRUSTED_CAS = "cm_trusted_cas"; + // let NS Owners choose with <ns>.certman aaf ignoreIPs" to ignoreIP Check for + // Configs + // Probably only want to allow in a DEV Env. + public static final String CM_ALLOW_IGNORE_IPS = "cm_allow_ignore_ips"; + // Docker doesn't have a default DNS. The property turns off IP Checking of DNSs + // before creating. + public static final String CM_ALWAYS_IGNORE_IPS = "cm_always_ignore_ips"; + + public static final String PATHFILTER_URLPATTERN = "pathfilter_urlpattern"; + public static final String PATHFILTER_STACK = "pathfilter_stack"; + public static final String PATHFILTER_NS = "pathfilter_ns"; + public static final String PATHFILTER_NOT_AUTHORIZED_MSG = "pathfilter_not_authorized_msg"; + + // This one should go unpublic + public static final String AAF_DEFAULT_REALM = "aaf_default_realm"; + private static String defaultRealm = "people.osaaf.org"; + + public static final String AAF_DOMAIN_SUPPORT = "aaf_domain_support"; + public static final String AAF_DOMAIN_SUPPORT_DEF = ".com:.org"; + + // OAUTH2 + public static final String AAF_OAUTH2_TOKEN_URL = "aaf_oauth2_token_url"; + public static final String AAF_OAUTH2_INTROSPECT_URL = "aaf_oauth2_introspect_url"; + public static final String AAF_ALT_OAUTH2_TOKEN_URL = "aaf_alt_oauth2_token_url"; + public static final String AAF_ALT_OAUTH2_INTROSPECT_URL = "aaf_alt_oauth2_introspect_url"; + public static final String AAF_ALT_OAUTH2_DOMAIN = "aaf_alt_oauth2_domain"; + public static final String AAF_ALT_CLIENT_ID = "aaf_alt_oauth2_client_id"; + public static final String AAF_ALT_CLIENT_SECRET = "aaf_alt_oauth2_client_secret"; + public static final String AAF_OAUTH2_HELLO_URL = "aaf_oauth2_hello_url"; + + public static void setDefaultRealm(Access access) { + try { + defaultRealm = logProp(access, Config.AAF_DEFAULT_REALM, logProp(access, Config.BASIC_REALM, + logProp(access, HOSTNAME, InetAddress.getLocalHost().getHostName()))); + } catch (UnknownHostException e) { + access.log(Level.INIT, "Unable to determine Hostname", e); + } + } + + public static HttpTaf configHttpTaf(Connector con, SecurityInfoC<HttpURLConnection> si, TrustChecker tc, CredVal up, + Lur lur, Object... additionalTafLurs) throws CadiException, LocatorException { + Access access = si.access; + RegistrationPropHolder rph; + try { + rph = new RegistrationPropHolder(access, 0); + } catch (UnknownHostException e2) { + throw new CadiException(e2); + } + ///////////////////////////////////////////////////// + // Setup AAFCon for any following + ///////////////////////////////////////////////////// + Class<?> aafConClass = loadClass(access, CADI_AAF_CON_DEF); + Object aafcon = null; + if (con != null && aafConClass != null && aafConClass.isAssignableFrom(con.getClass())) { + aafcon = con; + } else if (lur != null) { + Field f; + try { + f = lur.getClass().getField("aaf"); + aafcon = f.get(lur); + } catch (Exception e) { + access.log(Level.INIT, e); + } + } + + boolean hasDirectAAF = hasDirect("DirectAAFLur", additionalTafLurs); + // IMPORTANT! Don't attempt to load AAF Connector if there is no AAF URL + String aafURL = logProp(rph, AAF_URL, null); + if (!hasDirectAAF && aafcon == null && aafURL != null) { + aafcon = loadAAFConnector(si, aafURL); + } + + HttpTaf taf; + // Setup Host, in case Network reports an unusable Hostname (i.e. VTiers, VPNs, + // etc) + String hostname = logProp(access, HOSTNAME, null); + if (hostname == null) { + try { + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e1) { + throw new CadiException("Unable to determine Hostname", e1); + } + } + + access.log(Level.INIT, "Hostname set to", hostname); + // Get appropriate TAFs + ArrayList<Priori<HttpTaf>> htlist = new ArrayList<>(); + + ///////////////////////////////////////////////////// + // Add a Denial of Service TAF + // Note: how IPs and IDs are added are up to service type. + // They call "DenialOfServiceTaf.denyIP(String) or denyID(String) + ///////////////////////////////////////////////////// + htlist.add(new Priori<HttpTaf>(new DenialOfServiceTaf(access), 0)); + + ///////////////////////////////////////////////////// + // Configure Client Cert TAF + ///////////////////////////////////////////////////// + X509Taf x509TAF = null; + String truststore = logProp(access, CADI_TRUSTSTORE, null); + if (truststore != null) { + String truststorePwd = access.getProperty(CADI_TRUSTSTORE_PASSWORD, null); + if (truststorePwd != null) { + if (truststorePwd.startsWith(Symm.ENC)) { + try { + access.decrypt(truststorePwd, false); + } catch (IOException e) { + throw new CadiException(CADI_TRUSTSTORE_PASSWORD + " cannot be decrypted", e); + } + } + try { + x509TAF = new X509Taf(access, lur); + htlist.add(new Priori<HttpTaf>(x509TAF, 10)); + access.log(Level.INIT, "Certificate Authorization enabled"); + } catch (SecurityException | IllegalArgumentException e) { + access.log(Level.INIT, + "AAFListedCertIdentity cannot be instantiated. Certificate Authorization is now disabled", + e); + } catch (CertificateException e) { + access.log(Level.INIT, "Certificate Authorization failed, it is disabled", e); + } catch (NoSuchAlgorithmException e) { + access.log(Level.INIT, "Certificate Authorization failed, wrong Security Algorithm", e); + } + } + } else { + access.log(Level.INIT, "Certificate Authorization not enabled"); + } + + ///////////////////////////////////////////////////// + // Configure Basic Auth (local content) + ///////////////////////////////////////////////////// + boolean hasOAuthDirectTAF = hasDirect("DirectOAuthTAF", additionalTafLurs); + String basicRealm = logProp(access, BASIC_REALM, null); + String aafCleanup = logProp(access, AAF_USER_EXPIRES, AAF_USER_EXPIRES_DEF); // Default is 10 mins + long userExp = Long.parseLong(aafCleanup); + boolean basicWarn = "TRUE".equals(access.getProperty(BASIC_WARN, "FALSE")); + + if (!hasDirectAAF) { + HttpTaf aaftaf = null; + if (!hasOAuthDirectTAF) { + if (basicRealm != null) { + @SuppressWarnings("unchecked") + Class<HttpTaf> obasicCls = (Class<HttpTaf>) loadClass(access, CADI_OBASIC_HTTP_TAF_DEF); + if (obasicCls != null) { + try { + String tokenurl = logProp(rph, Config.AAF_OAUTH2_TOKEN_URL, null); + String introspecturl = logProp(rph, Config.AAF_OAUTH2_INTROSPECT_URL, null); + if (tokenurl == null || introspecturl == null) { + access.log(Level.INIT, + "Both tokenurl and introspecturl are required. Oauth Authorization is disabled."); + } else { + // try to construct the TAF instance. Try without the CredVal first (original code), change + // to try with a CredVal paramater if it fails as the newer ONAP code contains this in the OBasicHttpTaf constructor + System.out.println("TokenURL="+ tokenurl + "; IntrospectURL="+introspecturl); + Constructor<HttpTaf> obasicConst = null; + try { + obasicConst = obasicCls.getConstructor(PropAccess.class, String.class, + String.class, String.class); + htlist.add(new Priori<HttpTaf>( + obasicConst.newInstance(access, basicRealm, tokenurl, introspecturl), 20)); + } catch (Exception e) { + obasicConst = obasicCls.getConstructor(PropAccess.class, CredVal.class, String.class, String.class, String.class); + htlist.add(new Priori<HttpTaf>( + obasicConst.newInstance(access, up, basicRealm, tokenurl, introspecturl), 20)); + } + + access.log(Level.INIT, "Oauth supported Basic Authorization is enabled"); + } + } catch (NoSuchMethodException | SecurityException | InstantiationException + | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + access.log(Level.INIT, e); + } + } else if (up != null) { + access.log(Level.INIT, "Basic Authorization is enabled using realm", basicRealm); + // Allow warning about insecure channel to be turned off + if (!basicWarn) { + access.log(Level.INIT, "WARNING! The basicWarn property has been set to false.", + " There will be no additional warning if Basic Auth is used on an insecure channel"); + } + BasicHttpTaf bht = new BasicHttpTaf(access, up, basicRealm, userExp, basicWarn); + for (Object o : additionalTafLurs) { + if (o instanceof CredValDomain) { + bht.add((CredValDomain) o); + } + } + if (x509TAF != null) { + x509TAF.add(bht); + } + htlist.add(new Priori<HttpTaf>(bht, 20)); + access.log(Level.INIT, "Basic Authorization is enabled"); + } + } else { + access.log(Level.INIT, + "Local Basic Authorization is disabled. Enable by setting basicRealm=<appropriate realm, i.e. my.att.com>"); + } + + ///////////////////////////////////////////////////// + // Configure AAF Driven Basic Auth + ///////////////////////////////////////////////////// + if (aafcon == null) { + access.log(Level.INIT, "AAF Connection (AAFcon) is null. Cannot create an AAF TAF"); + } else if (aafURL == null) { + access.log(Level.INIT, "No AAF URL in properties, Cannot create an AAF TAF"); + } else {// There's an AAF_URL... try to configure an AAF + String aafTafClassName = logProp(access, AAF_TAF_CLASS, AAF_TAF_CLASS_DEF); + // Only 2.0 available at this time + if (AAF_TAF_CLASS_DEF.equals(aafTafClassName)) { + try { + Class<?> aafTafClass = loadClass(access, aafTafClassName); + if (aafTafClass != null) { + Constructor<?> cstr = aafTafClass.getConstructor(Connector.class, boolean.class, + AbsUserCache.class); + if (cstr != null) { + if (lur instanceof AbsUserCache) { + aaftaf = (HttpTaf) cstr.newInstance(aafcon, basicWarn, lur); + } else { + cstr = aafTafClass.getConstructor(Connector.class, boolean.class); + if (cstr != null) { + aaftaf = (HttpTaf) cstr.newInstance(aafcon, basicWarn); + } + } + if (aaftaf == null) { + access.log(Level.INIT, "ERROR! AAF TAF Failed construction. NOT Configured"); + } else { + access.log(Level.INIT, "AAF TAF Configured to ", aafURL); + // Note: will add later, after all others configured + } + } + } else { + access.log(Level.INIT, + "There is no AAF TAF class available: %s. AAF TAF not configured.", + aafTafClassName); + } + } catch (Exception e) { + access.log(Level.INIT, "ERROR! AAF TAF Failed construction. NOT Configured", e); + } + } + } + } + + ///////////////////////////////////////////////////// + // Configure OAuth TAF + ///////////////////////////////////////////////////// + if (!hasOAuthDirectTAF) { + String oauthTokenUrl = logProp(rph, Config.AAF_OAUTH2_TOKEN_URL, null); + Class<?> oadtClss; + try { + oadtClss = Class.forName(OAUTH_DIRECT_TAF); + } catch (ClassNotFoundException e1) { + oadtClss = null; + access.log(Level.DEBUG, e1); + } + if (additionalTafLurs != null && additionalTafLurs.length > 0 + && (oadtClss != null && additionalTafLurs[0].getClass().isAssignableFrom(oadtClss))) { + htlist.add(new Priori<HttpTaf>((HttpTaf) additionalTafLurs[0], 30)); + String[] array = new String[additionalTafLurs.length - 1]; + if (array.length > 0) { + System.arraycopy(htlist, 1, array, 0, array.length); + } + additionalTafLurs = array; + access.log(Level.INIT, "OAuth2 Direct is enabled"); + } else if (oauthTokenUrl != null) { + String oauthIntrospectUrl = logProp(rph, Config.AAF_OAUTH2_INTROSPECT_URL, null); + @SuppressWarnings("unchecked") + Class<HttpTaf> oaTCls = (Class<HttpTaf>) loadClass(access, OAUTH_HTTP_TAF); + if (oaTCls != null) { + Class<?> oaTTmgrCls = loadClass(access, OAUTH_TOKEN_MGR); + if (oaTTmgrCls != null) { + try { + Method oaTTmgrGI = oaTTmgrCls.getMethod("getInstance", PropAccess.class, String.class, + String.class); + Object oaTTmgr = oaTTmgrGI.invoke(null /* this is static method */, access, + oauthTokenUrl, oauthIntrospectUrl); + Constructor<HttpTaf> oaTConst = oaTCls.getConstructor(Access.class, oaTTmgrCls); + htlist.add(new Priori<HttpTaf>(oaTConst.newInstance(access, oaTTmgr), 30)); + access.log(Level.INIT, "OAuth2 TAF is enabled"); + } catch (NoSuchMethodException | SecurityException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException | InstantiationException e) { + access.log(Level.INIT, "OAuth2HttpTaf cannot be instantiated. OAuth2 is disabled", e); + } + } + } + } else { + access.log(Level.INIT, "OAuth TAF is not configured"); + } + } + + ///////////////////////////////////////////////////// + // Adding BasicAuth (AAF) last, after other primary Cookie Based + // Needs to be before Cert... see below + ///////////////////////////////////////////////////// + if (aaftaf != null) { + htlist.add(new Priori<HttpTaf>(aaftaf, 40)); + } + } + + ///////////////////////////////////////////////////// + // Any Additional Tafs passed in Constructor + ///////////////////////////////////////////////////// + if (additionalTafLurs != null) { + int i = 0; + for (Object additional : additionalTafLurs) { + if (additional instanceof BasicHttpTaf) { + BasicHttpTaf ht = (BasicHttpTaf) additional; + for (Object cv : additionalTafLurs) { + if (cv instanceof CredValDomain) { + ht.add((CredValDomain) cv); + access.printf(Level.INIT, "%s Authentication is enabled", cv); + } + } + htlist.add(new Priori<HttpTaf>(ht, 50 + i++)); + } else if (additional instanceof HttpTaf) { + HttpTaf ht = (HttpTaf) additional; + htlist.add(new Priori<HttpTaf>(ht, 50 + i++)); + access.printf(Level.INIT, "%s Authentication is enabled", additional.getClass().getSimpleName()); + } else if (hasOAuthDirectTAF) { + Class<?> daupCls; + try { + daupCls = Class.forName("org.onap.aaf.auth.direct.DirectAAFUserPass"); + } catch (ClassNotFoundException e) { + daupCls = null; + access.log(Level.INIT, e); + } + if (daupCls != null && additional.getClass().isAssignableFrom(daupCls)) { + htlist.add(new Priori<HttpTaf>( + new BasicHttpTaf(access, (CredVal) additional, basicRealm, userExp, basicWarn), + 50 + i++)); + access.printf(Level.INIT, "Direct BasicAuth Authentication is enabled", + additional.getClass().getSimpleName()); + } + } + } + } + + // Add BasicAuth, if any, to x509Taf + if (x509TAF != null) { + for (Priori<HttpTaf> ht : htlist) { + if (ht.t instanceof BasicHttpTaf) { + x509TAF.add((BasicHttpTaf) ht.t); + } + } + } + + ///////////////////////////////////////////////////// + // Additional TAFs by Plugin + ///////////////////////////////////////////////////// + Priori.add(access, CADI_ADD_TAFS, htlist); + + ///////////////////////////////////////////////////// + // Create EpiTaf from configured TAFs + ///////////////////////////////////////////////////// + if (htlist.size() == 1) { + // just return the one + taf = htlist.get(0).t; + } else { + Collections.sort(htlist); + HttpTaf[] htarray = new HttpTaf[htlist.size()]; + int i = -1; + StringBuilder sb = new StringBuilder("Tafs processed in this order:\n"); + for (Priori<HttpTaf> pht : htlist) { + htarray[++i] = pht.t; + sb.append(" "); + sb.append(pht.t.getClass().getName()); + sb.append('('); + sb.append(pht.priority); + sb.append(")\n"); + } + access.log(Level.INIT, sb); + + Locator<URI> locator = loadLocator(si, aafURL); + + taf = new HttpEpiTaf(access, locator, tc, htarray); // ok to pass locator == null + String level = logProp(access, CADI_LOGLEVEL, null); + if (level != null) { + access.setLogLevel(Level.valueOf(level)); + } + } + + return taf; + } + + public static String logProp(RegistrationPropHolder rph, String tag, String def) { + String rv = rph.access().getProperty(tag, def); + if (rv == null) { + rph.access().log(Level.INIT, tag, "is not explicitly set"); + } else { + rv = rph.replacements("Config.logProp", rv, null, null); + rph.access().log(Level.INIT, tag, "is set to", rv); + } + return rv; + + } + + public static String logProp(Access access, String tag, String def) { + String rv = access.getProperty(tag, def); + if (rv == null) { + access.log(Level.INIT, tag, "is not explicitly set"); + } else { + access.log(Level.INIT, tag, "is set to", rv); + } + return rv; + } + + public static Lur configLur(SecurityInfoC<HttpURLConnection> si, Connector con, Object... additionalTafLurs) + throws CadiException { + Access access = si.access; + RegistrationPropHolder rph; + try { + rph = new RegistrationPropHolder(access, 0); + } catch (UnknownHostException e2) { + throw new CadiException(e2); + } + + List<Priori<Lur>> lurs = new ArrayList<>(); + + ///////////////////////////////////////////////////// + // Configure a Local Property Based RBAC/LUR + ///////////////////////////////////////////////////// + try { + String users = access.getProperty(USERS, null); + String groups = access.getProperty(GROUPS, null); + + if (groups != null || users != null) { + LocalLur ll = new LocalLur(access, users, groups); // note b64==null is ok.. just means no encryption. + lurs.add(new Priori<Lur>(ll, 10)); + + String writeto = access.getProperty(WRITE_TO, null); + if (writeto != null) { + String msg = UsersDump.updateUsers(writeto, ll); + if (msg != null) { + access.log(Level.INIT, "ERROR! Error Updating ", writeto, "with roles and users:", msg); + } + } + } + } catch (IOException e) { + throw new CadiException(e); + } + + ///////////////////////////////////////////////////// + // Configure the OAuth Lur (if any) + ///////////////////////////////////////////////////// + String tokenUrl = logProp(rph, AAF_OAUTH2_TOKEN_URL, null); + String introspectUrl = logProp(rph, AAF_OAUTH2_INTROSPECT_URL, null); + if (tokenUrl != null && introspectUrl != null) { + try { + Class<?> olurCls = loadClass(access, CADI_OLUR_CLASS_DEF); + if (olurCls != null) { + Constructor<?> olurCnst = olurCls.getConstructor(PropAccess.class, String.class, String.class); + Lur olur = (Lur) olurCnst.newInstance(access, tokenUrl, introspectUrl); + lurs.add(new Priori<Lur>(olur, 20)); + access.log(Level.INIT, "OAuth2 LUR enabled"); + } else { + access.log(Level.INIT, "AAF/OAuth LUR plugin is not available."); + } + } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException e) { + String msg = e.getMessage(); + if (msg == null && e.getCause() != null) { + msg = e.getCause().getMessage(); + } + access.log(Level.INIT, "AAF/OAuth LUR is not instantiated.", msg, e); + } + } else { + access.log(Level.INIT, "OAuth2 Lur disabled"); + } + + if (con != null) { // try to reutilize connector + lurs.add(new Priori<Lur>(con.newLur(), 30)); + } else { + ///////////////////////////////////////////////////// + // Configure the AAF Lur (if any) + ///////////////////////////////////////////////////// + String aafURL = logProp(rph, AAF_URL, null); // Trigger Property + String aafEnv = access.getProperty(AAF_ENV, null); + if (aafEnv == null && aafURL != null && access instanceof PropAccess) { // set AAF_ENV from AAF_URL + int ec = aafURL.indexOf("envContext="); + if (ec > 0) { + ec += 11; // length of envContext= + int slash = aafURL.indexOf('/', ec); + if (slash > 0) { + aafEnv = aafURL.substring(ec, slash); + ((PropAccess) access).setProperty(AAF_ENV, aafEnv); + access.printf(Level.INIT, "Setting aafEnv to %s from aaf_url value", aafEnv); + } + } + } + + // Don't configure AAF if it is using DirectAccess + if (!hasDirect("DirectAAFLur", additionalTafLurs)) { + if (aafURL == null) { + access.log(Level.INIT, "No AAF LUR properties, AAF will not be loaded"); + } else {// There's an AAF_URL... try to configure an AAF + String aafLurClassStr = logProp(access, AAF_LUR_CLASS, AAF_V2_0_AAF_LUR_PERM); + //////////// AAF Lur 2.0 ///////////// + if (aafLurClassStr != null && aafLurClassStr.startsWith(AAF_V2_0)) { + try { + Object aafcon = loadAAFConnector(si, aafURL); + if (aafcon == null) { + access.log(Level.INIT, "AAF LUR class,", aafLurClassStr, + "cannot be constructed without valid AAFCon object."); + } else { + Class<?> aafAbsAAFCon = loadClass(access, AAF_V2_0_AAFCON); + if (aafAbsAAFCon != null) { + Method mNewLur = aafAbsAAFCon.getMethod("newLur"); + Object aaflur = mNewLur.invoke(aafcon); + + if (aaflur == null) { + access.log(Level.INIT, "ERROR! AAF LUR Failed construction. NOT Configured"); + } else { + access.log(Level.INIT, "AAF LUR Configured to ", aafURL); + lurs.add(new Priori<Lur>((Lur) aaflur, 40)); + String debugIDs = logProp(access, Config.AAF_DEBUG_IDS, null); + if (debugIDs != null && aaflur instanceof CachingLur) { + ((CachingLur<?>) aaflur).setDebug(debugIDs); + } + } + } + } + } catch (Exception e) { + access.log(e, "AAF LUR class,", aafLurClassStr, + "could not be constructed with given Constructors."); + } + } + } + } + } + + ///////////////////////////////////////////////////// + // Any Additional passed in Constructor + ///////////////////////////////////////////////////// + if (additionalTafLurs != null) { + int i = 0; + for (Object additional : additionalTafLurs) { + if (additional instanceof Lur) { + lurs.add(new Priori<Lur>((Lur) additional, 50 + i++)); + access.log(Level.INIT, additional); + } + } + } + + ///////////////////////////////////////////////////// + // Additional LURs by Plugin + ///////////////////////////////////////////////////// + Priori.add(access, CADI_ADD_LURS, lurs); + + ///////////////////////////////////////////////////// + // Return a Lur based on how many there are... + ///////////////////////////////////////////////////// + switch (lurs.size()) { + case 0: + access.log(Level.INIT, "WARNING! No CADI LURs configured"); + // Return a NULL Lur that does nothing. + return new NullLur(); + case 1: + return lurs.get(0).t; // Only one, just return it, save processing + default: + // Multiple Lurs, use EpiLUR to handle + Collections.sort(lurs); + Lur[] la = new Lur[lurs.size()]; + int i = -1; + StringBuilder sb = new StringBuilder("Lurs processed in this order:\n"); + for (Priori<Lur> pht : lurs) { + la[++i] = pht.t; + sb.append(" "); + sb.append(pht.t.getClass().getName()); + sb.append('('); + sb.append(pht.priority); + sb.append(")\n"); + } + access.log(Level.INIT, sb); + return new EpiLur(la); + } + } + + private static boolean hasDirect(String simpleClassName, Object[] additionalTafLurs) { + if (additionalTafLurs != null) { + for (Object tf : additionalTafLurs) { + if (tf.getClass().getSimpleName().equals(simpleClassName)) { + return true; + } + } + } + return false; + } + + @SuppressWarnings("unchecked") + public static Object loadAAFConnector(SecurityInfoC<?> si, String aafURL) { + Access access = si.access; + Object aafcon = null; + Class<?> aafConClass = null; + + try { + if (aafURL != null) { + String aafConnector = access.getProperty(AAF_CONNECTOR_CLASS, AAF_V2_0_AAF_CON_HTTP); + if (AAF_V2_0_AAF_CON_HTTP.equals(aafConnector)) { + aafConClass = loadClass(access, AAF_V2_0_AAF_CON_HTTP); + if (aafConClass != null) { + for (Constructor<?> c : aafConClass.getConstructors()) { + List<Object> lo = new ArrayList<>(); + for (Class<?> pc : c.getParameterTypes()) { + if (pc.equals(Access.class)) { + lo.add(access); + } else if (pc.equals(Locator.class)) { + lo.add(loadLocator((SecurityInfoC<HttpURLConnection>) si, aafURL)); + } + } + if (c.getParameterTypes().length != lo.size()) { + continue; // back to another Constructor + } else { + aafcon = c.newInstance(lo.toArray()); + } + break; + } + } + } + if (aafcon != null) { + String mechid = logProp(access, Config.AAF_APPID, null); + String pass = access.getProperty(Config.AAF_APPPASS, null); + if (mechid != null && pass != null) { + try { + Method basicAuth = aafConClass.getMethod("basicAuth", String.class, String.class); + basicAuth.invoke(aafcon, mechid, pass); + } catch (NoSuchMethodException nsme) { + access.log(Level.NONE, nsme); + // it's ok, don't use + } + } + } + } + } catch (Exception e) { + access.log(e, "AAF Connector could not be constructed with given Constructors."); + } + + return aafcon; + } + + public static Class<?> loadClass(Access access, String className) { + Class<?> cls = null; + try { + cls = access.classLoader().loadClass(className); + } catch (ClassNotFoundException cnfe) { + access.log(Level.NONE, cnfe); + try { + cls = access.getClass().getClassLoader().loadClass(className); + } catch (ClassNotFoundException cnfe2) { + access.log(Level.NONE, cnfe2); + // just return null + } + } + return cls; + } + + @SuppressWarnings("unchecked") + public static Locator<URI> loadLocator(SecurityInfoC<HttpURLConnection> si, final String _url) + throws LocatorException { + Access access = si.access; + Locator<URI> locator = null; + if (_url == null) { + access.log(Level.INIT, "No URL passed to 'loadLocator'. Disabled"); + } else { + try { + Class<?> aalCls = Class.forName("org.onap.aaf.cadi.aaf.v2_0.AbsAAFLocator"); + Method aalMth = aalCls.getMethod("create", String.class, String.class); + int colon = _url.lastIndexOf(':'); + if (colon >= 0) { + int slash = _url.indexOf('/', colon); + String version; + if (slash < 0) { + version = _url.substring(colon + 1); + } else { + version = _url.substring(colon + 1, slash); + } + slash = _url.lastIndexOf('/', colon); + if (slash >= 0) { + Object aal = aalMth.invoke(null/* static */, _url.substring(slash + 1, colon), version); + return (Locator<URI>) aal; + } + } + } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException e) { + String msg; + char quote; + if (e.getCause() != null) { + msg = e.getCause().getMessage(); + quote = '"'; + } else { + msg = "-"; + quote = ' '; + } + access.printf(Level.DEBUG, "Configured AbsAAFLocator not found%c%s%cContinuing Locator creation ", + quote, msg, quote); + } + // String url = _url.replace("/AAF_NS.", "/%C%CID%AAF_NS."); + // String root_ns = access.getProperty(Config.AAF_ROOT_NS, null); + String url; + RegistrationPropHolder rph; + try { + rph = new RegistrationPropHolder(access, 0); + url = rph.replacements("Config.loadLocator", _url, null, null); + access.printf(Level.INFO, "loadLocator URL is %s", url); + } catch (UnknownHostException | CadiException e1) { + throw new LocatorException(e1); + } + + /** + * Simplify logic - if we have a URL with /locate/ in it, we use the default locator. + * If we have an explicitly set locator from configuration, we use that one. + * Otherwise we fall back to the SingleEndpointLocator, basically default normal HTTP client behavior. + */ + String aaf_locator_class = null; + if (url.contains("/locate/")) { + aaf_locator_class = AAF_LOCATOR_CLASS_DEF; + } else if (si.access.getProperty(Config.AAF_LOCATOR_CLASS, null) != null) { + aaf_locator_class = si.access.getProperty(Config.AAF_LOCATOR_CLASS, null); + } + if (aaf_locator_class == null) { + aaf_locator_class = Config.AAF_LOCATOR_CLASS_SINGLE; + } + + try { + Class<?> lcls = loadClass(access,aaf_locator_class); + if (lcls==null) { + throw new CadiException("Need to include aaf-cadi-aaf jar for AAFLocator"); + } + // First check for preloaded + try { + Method meth = lcls.getMethod("create",Access.class,String.class); + locator = (Locator<URI>)meth.invoke(null,access,url); + } catch (Exception e) { + access.log(Level.NONE, "(Not fatal) Cannot load by create(String)", e); + } + if (locator==null) { + URI locatorURI = new URI(url); + FixURIinfo fui = new FixURIinfo(locatorURI); + Constructor<?> cnst = lcls.getConstructor(SecurityInfoC.class,URI.class); + locator = (Locator<URI>)cnst.newInstance(new Object[] {si,locatorURI}); + int port = fui.getPort(); + String portS = port<0?"":(":"+port); + + access.log(Level.INFO, "AAFLocator [" + locator.getClass().getSimpleName() + "] enabled using " + locatorURI.getScheme() +"://"+fui.getHost() + portS); + } else { + access.log(Level.INFO, "AAFLocator [" + locator.getClass().getSimpleName() + "] enabled using " + url); + } + } catch (InvocationTargetException e) { + if (e.getTargetException() instanceof LocatorException) { + throw (LocatorException)e.getTargetException(); + } + access.log(Level.INIT,e.getTargetException().getMessage(),"AAFLocator for",url,"could not be created.",e); + } catch (Exception e) { + access.log(Level.INIT,"AAFLocator for",url,"could not be created.",e); + } + } + return locator; + } + + // Set by CSP, or is hostname. + public static String getDefaultRealm() { + return defaultRealm; + } + + public static String getAAFLocateUrl(Access access) { + String rv = null; + String cont = access.getProperty(AAF_LOCATOR_CONTAINER,null); + if(cont!=null) { + rv = access.getProperty(AAF_LOCATE_URL + '.' +cont, null); + } + if(rv==null) { + rv = access.getProperty(AAF_LOCATE_URL, null); + } + return rv; + } + + private static class Priori<T> implements Comparable<Priori<T>> { + public final T t; + public final int priority; + + public Priori(final T t, final int priority) { + this.t = t; + this.priority = priority; + } + + @Override + public int compareTo(Priori<T> o) { + if(priority==o.priority) { + return 0; + } else if(priority<o.priority) { + return -1; + } else { + return 1; + } + } + public static<T> void add(Access access, final String tag, List<Priori<T>> list) { + String plugins = access.getProperty(tag, null); + if(plugins!=null) { + access.log(Level.INIT, "Adding TAF Plugins: ", plugins); + for(String tafs : Split.splitTrim(';', plugins)) { + String[] pluginArray = Split.splitTrim(',', tafs); + String clssn = null; + int priority = 60; + switch(pluginArray.length) { + case 0: + break; + case 1: + clssn = tafs; + break; + default: + clssn = pluginArray[0]; + try { + priority = Integer.parseInt(pluginArray[1]); + } catch (NumberFormatException nfe) { + access.printf(Level.ERROR, "%s format is <classname>,priority[;...]\n",CADI_ADD_TAFS); + } + } + + if(clssn!=null) { + Class<?> cls = loadClass(access, clssn); + if(cls!=null) { + try { + @SuppressWarnings("unchecked") + Constructor<T> cnst = (Constructor<T>)cls.getConstructor(Access.class); + try { + list.add(new Priori<T>(cnst.newInstance(access),priority)); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + String hostname = access.getProperty(Config.HOSTNAME,null); + if(hostname==null) { + access.printf(Level.ERROR, "%s cannot be constructed on this machine. Set valid 'hostname' in your properties\n",clssn); + } else { + access.printf(Level.ERROR, "%s cannot be constructed on %s with Access.\n",clssn, hostname); + } + } + } catch (NoSuchMethodException | SecurityException e) { + access.printf(Level.ERROR, "%s needs a Constructor taking Access as sole param.\n",clssn); + } + } + } + } + } + } + } +} + diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/Get.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/Get.java new file mode 100644 index 00000000..4bceea24 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/Get.java @@ -0,0 +1,97 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk.apps + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.config; + +import java.lang.reflect.Method; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.Access.Level; + +public interface Get { + public String get(String name, String def, boolean print); + + + /** + * A class for Getting info out of "JavaBean" format + * @author Jonathan + * + */ + public static class Bean implements Get { + private Object bean; + private Class<?> bc; + private Class<?>[] params; + private Object[] args; + + public Bean(Object bean) { + this.bean = bean; + bc = bean.getClass(); + params = new Class<?>[0]; // note, this will allow to go out of scope after config + args = new Object[0]; + } + + public String get(String name, String def, boolean print) { + String str = null; + String gname = "get"+Character.toUpperCase(name.charAt(0))+name.substring(1); + try { + Method meth = bc.getMethod(gname, params); + Object obj = meth.invoke(bean, args); + str = obj==null?null:obj.toString(); // easy string convert... + } catch (Exception e) { + } + + // Take def if nothing else + if (str==null) { + str = def; + // don't log defaults + } else { + str = str.trim(); // this is vital in Property File based values, as spaces can hide easily + } + // Note: Can't log during configuration + return str; + } + } + + public static Get NULL = new Get() { + public String get(String name, String def, boolean print) { + return def; + } + }; + + public static class AccessGet implements Get { + private Access access; + public AccessGet(Access access) { + this.access = access; + } + public String get(String name, String def, boolean print) { + String gotten = access.getProperty(name, def); + if (print) { + if (gotten == null) { + access.log(Level.INIT,name, "is not set"); + } else { + access.log(Level.INIT,name, "is set to", gotten); + } + } + return gotten; + } + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/GetAccess.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/GetAccess.java new file mode 100644 index 00000000..ed3faa78 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/GetAccess.java @@ -0,0 +1,57 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.config; + +import org.onap.ccsdk.apps.cadi.PropAccess; + +public class GetAccess extends PropAccess { + private final Get getter; + + public GetAccess(Get getter) { + super(new String[]{"cadi_prop_files="+getter.get("cadi_prop_files", null, true)}); + this.getter = getter; + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.PropAccess#getProperty(java.lang.String, java.lang.String) + */ + @Override + public String getProperty(String tag, String def) { + String rv; + rv = super.getProperty(tag, null); + if (rv==null && getter!=null) { + rv = getter.get(tag, null, true); + } + return rv==null?def:rv; + } + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.PropAccess#getProperty(java.lang.String) + */ + @Override + public String getProperty(String tag) { + return getProperty(tag, null); + } + + public Get get() { + return getter; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/MultiGet.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/MultiGet.java new file mode 100644 index 00000000..f32484b7 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/MultiGet.java @@ -0,0 +1,42 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.config; + +public class MultiGet implements Get { + private Get[] getters; + + public MultiGet(Get ... getters) { + this.getters = getters; + } + + @Override + public String get(String name, String def, boolean print) { + String str; + for (Get getter : getters) { + str = getter.get(name, null, print); + if (str!=null) + return str; + } + return def; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/RegistrationPropHolder.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/RegistrationPropHolder.java new file mode 100644 index 00000000..17d0279e --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/RegistrationPropHolder.java @@ -0,0 +1,312 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + */ + +package org.onap.ccsdk.apps.cadi.config; + +import java.net.Inet4Address; +import java.net.UnknownHostException; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.util.Split; + +public class RegistrationPropHolder { + private final String PUBLIC_NAME="%NS.%N"; + private final String REGI="RegistrationProperty: %s='%s'"; + private final Access access; + public String hostname; + private int port; + public String public_fqdn; + private Integer public_port; + public Float latitude; + public Float longitude; + public final String default_fqdn; + public final String default_container_ns; + public final String default_name; + public final String lentries; + public final String lcontainer; + public final String default_container; + private static boolean firstlog = true; + + public RegistrationPropHolder(final Access access, final int port) throws UnknownHostException, CadiException { + this.access = access; + StringBuilder errs = new StringBuilder(); + String str; + this.port = port; + + lentries=access.getProperty(Config.AAF_LOCATOR_ENTRIES,""); + default_container = access.getProperty(Config.AAF_LOCATOR_CONTAINER, ""); + if(firstlog) { + access.printf(Level.INIT, REGI,"default_container",default_container); + } + if(!default_container.isEmpty()) { + lcontainer=',' + default_container; // "" makes a blank default Public Entry + str = access.getProperty(Config.AAF_LOCATOR_PUBLIC_PORT+'.'+default_container, null); + if(str==null) { + str = access.getProperty(Config.AAF_LOCATOR_PUBLIC_PORT, null); + } + } else { + lcontainer=default_container; + str = access.getProperty(Config.AAF_LOCATOR_PUBLIC_PORT, null); + } + if(str!=null) { + public_port=Integer.decode(str); + } + if(firstlog) { + access.printf(Level.INIT, "RegistrationProperty: public_port='%d'",public_port); + } + + hostname = access.getProperty(Config.HOSTNAME, null); + if (hostname==null) { + hostname = Inet4Address.getLocalHost().getHostName(); + } + if (hostname==null) { + mustBeDefined(errs,Config.HOSTNAME); + } + if(firstlog) { + access.printf(Level.INIT, REGI,"hostname",hostname); + } + + public_fqdn = access.getProperty(Config.AAF_LOCATOR_PUBLIC_FQDN, hostname); + if(firstlog) { + access.printf(Level.INIT, REGI,"public_fqdn",public_fqdn); + } + + // Allow Container to reset the standard name for public + String container_public_name = access.getProperty(Config.AAF_LOCATOR_PUBLIC_NAME+'.'+default_container, null); + if(container_public_name==null) { + container_public_name = access.getProperty(Config.AAF_LOCATOR_PUBLIC_NAME, null); + if(container_public_name==null) { + container_public_name = access.getProperty(Config.AAF_LOCATOR_NAME, PUBLIC_NAME); + } + } + default_name = container_public_name; + + if(firstlog) { + access.printf(Level.INIT, REGI,"default_name",default_name); + } + + latitude=null; + String slatitude = access.getProperty(Config.CADI_LATITUDE, null); + if(slatitude == null) { + mustBeDefined(errs,Config.CADI_LATITUDE); + } else { + latitude = Float.parseFloat(slatitude); + } + if(firstlog) { + access.printf(Level.INIT, REGI,"latitude",slatitude); + } + + longitude=null; + String slongitude = access.getProperty(Config.CADI_LONGITUDE, null); + if(slongitude == null) { + mustBeDefined(errs,Config.CADI_LONGITUDE); + } else { + longitude = Float.parseFloat(slongitude); + } + if(firstlog) { + access.printf(Level.INIT, REGI,"longitude",slongitude); + } + + String dot_le; + // Note: only one of the ports can be public... Therefore, only the last + for(String le : Split.splitTrim(',', lcontainer)) { + dot_le = le.isEmpty()?le :"."+le; + str = access.getProperty(Config.AAF_LOCATOR_PUBLIC_FQDN+dot_le,null); + if( str != null && !str.isEmpty()) { + public_fqdn=str; + if(firstlog) { + access.printf(Level.INIT, "RegistrationProperty: public_hostname(overloaded by %s)='%s'",dot_le,public_fqdn); + } + } + } + + default_fqdn = access.getProperty(Config.AAF_LOCATOR_FQDN, hostname); + if(firstlog) { + access.printf(Level.INIT, REGI,"default_fqdn",default_fqdn); + } + default_container_ns = access.getProperty(Config.AAF_LOCATOR_CONTAINER_NS,""); + if(firstlog) { + access.printf(Level.INIT, REGI,"default_container_ns",default_container_ns); + } + if(errs.length()>0) { + throw new CadiException(errs.toString()); + } + firstlog = false; + } + + private void mustBeDefined(StringBuilder errs, String propname) { + errs.append('\n'); + errs.append(propname); + errs.append(" must be defined."); + + } + + public String getEntryFQDN(final String entry, final String dot_le) { + String str; + if(public_fqdn!=null && dot_le.isEmpty()) { + str = public_fqdn; + } else { + str = access.getProperty(Config.AAF_LOCATOR_FQDN+dot_le, default_fqdn); + } + return replacements("RegistrationPropHolder.getEntryFQDN",str,entry,dot_le); + } + + public String getEntryName(final String entry, final String dot_le) { + String str; + if(dot_le.isEmpty()) { + str = default_name; + } else { + str = access.getProperty(Config.AAF_LOCATOR_NAME+dot_le, default_name); + } + return replacements("RegistrationPropHolder.getEntryName",str,entry,dot_le); + } + + public String getPublicEntryName(final String entry, final String dot_le) { + String str = access.getProperty(Config.AAF_LOCATOR_PUBLIC_NAME+dot_le, null); + if(str==null) { + str = access.getProperty(Config.AAF_LOCATOR_PUBLIC_NAME,null); + } + if(str==null) { + str = default_name; + } + return replacements("RegistrationPropHolder.getEntryName",str,entry,dot_le); + } + + + private String getNS(String dot_le) { + String ns; + ns = access.getProperty(Config.AAF_LOCATOR_APP_NS+dot_le,null); + if(ns==null) { + ns = access.getProperty(Config.AAF_LOCATOR_APP_NS, "AAF_NS"); + } + return ns; + } + + + public String replacements(final String fromCode, final String source, final String name, final String _dot_le) { + if(source == null) { + return ""; + } else if(source.isEmpty()) { + return source; + } + String value = source; + String dot_le; + if(_dot_le==null) { + dot_le = default_container.isEmpty()?"":'.'+default_container; + } else { + dot_le = _dot_le; + } + + String aaf_locator_host = access.getProperty(Config.AAF_LOCATE_URL+dot_le,null); + if(aaf_locator_host==null) { + aaf_locator_host = access.getProperty(Config.AAF_LOCATE_URL,null); + } + + String str; + if(aaf_locator_host!=null) { + if("https://AAF_LOCATE_URL".equals(value)) { + value = aaf_locator_host; + } else { + str = aaf_locator_host; + if(value.indexOf(Config.AAF_LOCATE_URL_TAG)>=0) { + if(!str.endsWith("/")) { + str+='/'; + } + if(!str.endsWith("/locate/")) { + str+="locate/"; + } + if(value.startsWith("http:")) { + value = value.replace("http://AAF_LOCATE_URL/", str); + } else { + value = value.replace("https://AAF_LOCATE_URL/", str); + + } + } + } + } + + int atC = value.indexOf("%C"); + if(atC>=0) { + // aaf_locator_container_ns + str = access.getProperty(Config.AAF_LOCATOR_CONTAINER_NS+dot_le, default_container_ns); + if(str.isEmpty()) { + value = value.replace("%CNS"+'.', str); + } + value = value.replace("%CNS", str); + + str = access.getProperty(Config.AAF_LOCATOR_CONTAINER+dot_le,default_container); + if(str.isEmpty()) { + value = value.replace("%C"+'.', str); + } + value = value.replace("%C", str); + } + + if(value.indexOf("%NS")>=0) { + str = getNS(dot_le); + if(str==null || str.isEmpty()) { + value = value.replace("%NS"+'.', ""); + } else { + value = value.replace("%NS", str); + } + } + + // aaf_root_ns + if(value.indexOf("AAF_NS")>=0) { + str = access.getProperty(Config.AAF_ROOT_NS, Config.AAF_ROOT_NS_DEF) + '.'; + String temp = value.replace("%AAF_NS.", str); + if(temp.equals(value)) { // intended + value = value.replace("AAF_NS.", str); // Backward Compatibility + } else { + value = temp; + } + } + + + if(value.indexOf('%')>=0) { + // These shouldn't be expected to have dot elements + if(name!=null) { + value = value.replace("%N", name); + } + if(default_fqdn!=null) { + value = value.replace("%DF", default_fqdn); + } + if(public_fqdn!=null) { + value = value.replace("%PH", public_fqdn); + } + } + access.printf(Level.DEBUG, + "RegistrationReplacement from %s, source: %s, dot_le: %s, value: %s", + fromCode,source,dot_le,value); + + return value; + } + + public int getEntryPort(final String dot_le) { + return public_port!=null && dot_le.isEmpty()? + public_port: + port; + } + + public Access access() { + return access; + } +}
\ No newline at end of file diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/SecurityInfo.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/SecurityInfo.java new file mode 100644 index 00000000..ad0c4c92 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/SecurityInfo.java @@ -0,0 +1,370 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.config; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.rmi.AccessException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.util.MaskFormatException; +import org.onap.ccsdk.apps.cadi.util.NetMask; +import org.onap.ccsdk.apps.cadi.util.Split; + +public class SecurityInfo { + private static final String SECURITY_ALGO = "RSA"; + private static final String HTTPS_PROTOCOLS = "https.protocols"; + private static final String JDK_TLS_CLIENT_PROTOCOLS = "jdk.tls.client.protocols"; + private static final String INITIALIZING_ERR_FMT = "Error initializing %s: %s"; + private static final String LOADED_FROM_CADI_PROPERTIES = "%s loaded from CADI Properties"; + private static final String LOADED_FROM_SYSTEM_PROPERTIES = "%s loaded from System Properties"; + + public static final String SSL_KEY_MANAGER_FACTORY_ALGORITHM; + + private SSLSocketFactory socketFactory; + private X509KeyManager[] x509KeyManager; + private X509TrustManager[] x509TrustManager; + public final String defaultAlias; + public final String defaultClientAlias; + private NetMask[] trustMasks; + private SSLContext context; + private HostnameVerifier maskHV; + public final Access access; + + // Change Key Algorithms for IBM's VM. Could put in others, if needed. + static { + if ("IBM Corporation".equalsIgnoreCase(System.getProperty("java.vm.vendor"))) { + SSL_KEY_MANAGER_FACTORY_ALGORITHM = "IbmX509"; + } else { + SSL_KEY_MANAGER_FACTORY_ALGORITHM = "SunX509"; + } + } + + + public SecurityInfo(final Access access) throws CadiException { + String msgHelp = ""; + try { + this.access = access; + // reuse DME2 Properties for convenience if specific Properties don't exist + + String str = access.getProperty(Config.CADI_ALIAS, null); + if(str==null || str.isEmpty()) { + defaultAlias = null; + } else { + defaultAlias = str; + } + + str = access.getProperty(Config.CADI_CLIENT_ALIAS, null); + if(str==null) { + defaultClientAlias = defaultAlias; + } else if(str.isEmpty()) { + // intentionally off, i.e. cadi_client_alias= + defaultClientAlias = null; + } else { + defaultClientAlias = str; + } + + msgHelp = String.format(INITIALIZING_ERR_FMT,"Keystore", access.getProperty(Config.CADI_KEYSTORE, "")); + initializeKeyManager(); + + msgHelp = String.format(INITIALIZING_ERR_FMT,"Truststore", access.getProperty(Config.CADI_TRUSTSTORE, "")); + initializeTrustManager(); + + + msgHelp = String.format(INITIALIZING_ERR_FMT,"Trustmasks", access.getProperty(Config.CADI_TRUST_MASKS, "")); + initializeTrustMasks(); + + msgHelp = String.format(INITIALIZING_ERR_FMT,"HTTP Protocols", "access properties"); + setHTTPProtocols(access); + + msgHelp = String.format(INITIALIZING_ERR_FMT,"Context", "TLS"); + context = SSLContext.getInstance("TLS"); + context.init(x509KeyManager, x509TrustManager, null); + SSLContext.setDefault(context); + socketFactory = context.getSocketFactory(); + } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | CertificateException | UnrecoverableKeyException | IOException e) { + throw new CadiException(msgHelp,e); + } + } + + public static void setHTTPProtocols(Access access) { + String httpsProtocols = System.getProperty(Config.HTTPS_PROTOCOLS); + if(httpsProtocols!=null) { + access.printf(Level.INIT, LOADED_FROM_SYSTEM_PROPERTIES, HTTPS_PROTOCOLS); + } else { + httpsProtocols = access.getProperty(Config.HTTPS_PROTOCOLS,null); + if(httpsProtocols!=null) { + access.printf(Level.INIT, LOADED_FROM_CADI_PROPERTIES, HTTPS_PROTOCOLS); + } else { + httpsProtocols = access.getProperty(HTTPS_PROTOCOLS, Config.HTTPS_PROTOCOLS_DEFAULT); + access.printf(Level.INIT, "%s set by %s in CADI Properties",Config.HTTPS_PROTOCOLS,Config.CADI_PROTOCOLS); + } + // This needs to be set when people do not. + System.setProperty(HTTPS_PROTOCOLS, httpsProtocols); + } + String httpsClientProtocols = System.getProperty(JDK_TLS_CLIENT_PROTOCOLS,null); + if(httpsClientProtocols!=null) { + access.printf(Level.INIT, LOADED_FROM_SYSTEM_PROPERTIES, JDK_TLS_CLIENT_PROTOCOLS); + } else { + httpsClientProtocols = access.getProperty(Config.HTTPS_CLIENT_PROTOCOLS, null); + if(httpsClientProtocols!=null) { + access.printf(Level.INIT, LOADED_FROM_CADI_PROPERTIES, Config.HTTPS_CLIENT_PROTOCOLS); + } else { + httpsClientProtocols = Config.HTTPS_PROTOCOLS_DEFAULT; + access.printf(Level.INIT, "%s set from %s",Config.HTTPS_CLIENT_PROTOCOLS, "Default Protocols"); + } + System.setProperty(JDK_TLS_CLIENT_PROTOCOLS, httpsClientProtocols); + } + } + + /** + * @return the scf + */ + public SSLSocketFactory getSSLSocketFactory() { + return socketFactory; + } + + public SSLContext getSSLContext() { + return context; + } + + /** + * @return the km + */ + public X509KeyManager[] getKeyManagers() { + return x509KeyManager; + } + + public void checkClientTrusted(X509Certificate[] certarr) throws CertificateException { + for (X509TrustManager xtm : x509TrustManager) { + xtm.checkClientTrusted(certarr, SECURITY_ALGO); + } + } + + public void checkServerTrusted(X509Certificate[] certarr) throws CertificateException { + for (X509TrustManager xtm : x509TrustManager) { + xtm.checkServerTrusted(certarr, SECURITY_ALGO); + } + } + + public void setSocketFactoryOn(HttpsURLConnection hsuc) { + hsuc.setSSLSocketFactory(socketFactory); + if (maskHV != null && !maskHV.equals(hsuc.getHostnameVerifier())) { + hsuc.setHostnameVerifier(maskHV); + } + } + + protected void initializeKeyManager() throws CadiException, IOException, NoSuchAlgorithmException, KeyStoreException, CertificateException, UnrecoverableKeyException { + String keyStore = access.getProperty(Config.CADI_KEYSTORE, null); + if(keyStore==null) { + return; + } else if (!new File(keyStore).exists()) { + throw new CadiException(keyStore + " does not exist"); + } + + String keyStorePasswd = access.getProperty(Config.CADI_KEYSTORE_PASSWORD, null); + keyStorePasswd = (keyStorePasswd == null) ? null : access.decrypt(keyStorePasswd, false); + if (keyStore == null || keyStorePasswd == null) { + x509KeyManager = new X509KeyManager[0]; + return; + } + + String keyPasswd = access.getProperty(Config.CADI_KEY_PASSWORD, null); + keyPasswd = (keyPasswd == null) ? keyStorePasswd : access.decrypt(keyPasswd, false); + + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(SSL_KEY_MANAGER_FACTORY_ALGORITHM); + + ArrayList<X509KeyManager> keyManagers = new ArrayList<>(); + File file; + for (String ksname : Split.splitTrim(',', keyStore)) { + String keystoreFormat; + if (ksname.endsWith(".p12") || ksname.endsWith(".pkcs12")) { + keystoreFormat = "PKCS12"; + } else { + keystoreFormat = "JKS"; + } + + file = new File(ksname); + if (file.exists()) { + FileInputStream fis = new FileInputStream(file); + try { + KeyStore ks = KeyStore.getInstance(keystoreFormat); + ks.load(fis, keyStorePasswd.toCharArray()); + keyManagerFactory.init(ks, keyPasswd.toCharArray()); + } finally { + fis.close(); + } + } + } + + StringBuilder sb = null; + for (KeyManager keyManager : keyManagerFactory.getKeyManagers()) { + if (keyManager instanceof X509KeyManager) { + X509KeyManager xkm = (X509KeyManager)keyManager; + keyManagers.add(xkm); + if(defaultAlias!=null) { + sb=new StringBuilder("X509 Chain\n"); + x509Info(sb,xkm.getCertificateChain(defaultAlias)); + } + if(defaultClientAlias!=null && !defaultClientAlias.equals(defaultAlias)) { + if(sb==null) { + sb = new StringBuilder(); + } else { + sb.append('\n'); + } + sb.append("X509 Client Chain\n"); + x509Info(sb,xkm.getCertificateChain(defaultAlias)); + } + } + } + x509KeyManager = new X509KeyManager[keyManagers.size()]; + keyManagers.toArray(x509KeyManager); + + if(sb!=null) { + access.log(Level.INIT, sb); + } + } + + private void x509Info(StringBuilder sb, X509Certificate[] chain) { + if(chain!=null) { + int i=0; + for(X509Certificate x : chain) { + sb.append(" "); + sb.append(i++); + sb.append(')'); + sb.append("\n Subject: "); + sb.append(x.getSubjectDN()); + sb.append("\n Issuer : "); + sb.append(x.getIssuerDN()); + sb.append("\n Expires: "); + sb.append(x.getNotAfter()); + sb.append('\n'); + } + } + } + + protected void initializeTrustManager() throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException, CadiException { + String trustStore = access.getProperty(Config.CADI_TRUSTSTORE, null); + if(trustStore==null) { + return; + } else if(!new File(trustStore).exists()) { + throw new CadiException(trustStore + " does not exist"); + } + + String trustStorePasswd = access.getProperty(Config.CADI_TRUSTSTORE_PASSWORD, null); + trustStorePasswd = (trustStorePasswd == null ) ? "changeit"/*defacto Java Trust Pass*/ : access.decrypt(trustStorePasswd, false); + + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(SSL_KEY_MANAGER_FACTORY_ALGORITHM); + File file; + for (String trustStoreName : Split.splitTrim(',',trustStore)) { + file = new File(trustStoreName); + if (file.exists()) { + FileInputStream fis = new FileInputStream(file); + try { + KeyStore ts = KeyStore.getInstance("JKS"); + ts.load(fis, trustStorePasswd.toCharArray()); + trustManagerFactory.init(ts); + } finally { + fis.close(); + } + } + } + + TrustManager trustManagers[] = trustManagerFactory.getTrustManagers(); + if (trustManagers == null || trustManagers.length == 0) { + return; + } + + x509TrustManager = new X509TrustManager[trustManagers.length]; + for (int i = 0; i < trustManagers.length; ++i) { + try { + x509TrustManager[i] = (X509TrustManager)trustManagers[i]; + } catch (ClassCastException e) { + access.log(Level.WARN, "Non X509 TrustManager", x509TrustManager[i].getClass().getName(), "skipped in SecurityInfo"); + } + } + } + + protected void initializeTrustMasks() throws AccessException { + String tips = access.getProperty(Config.CADI_TRUST_MASKS, null); + if (tips == null) { + return; + } + + access.log(Level.INIT, "Explicitly accepting valid X509s from", tips); + String[] ipsplit = Split.splitTrim(',', tips); + trustMasks = new NetMask[ipsplit.length]; + for (int i = 0; i < ipsplit.length; ++i) { + try { + trustMasks[i] = new NetMask(ipsplit[i]); + } catch (MaskFormatException e) { + throw new AccessException("Invalid IP Mask in " + Config.CADI_TRUST_MASKS, e); + } + } + + final HostnameVerifier origHV = HttpsURLConnection.getDefaultHostnameVerifier(); + maskHV = new HostnameVerifier() { + @Override + public boolean verify(final String urlHostName, final SSLSession session) { + try { + // This will pick up /etc/host entries as well as DNS + InetAddress ia = InetAddress.getByName(session.getPeerHost()); + for (NetMask tmask : trustMasks) { + if (tmask.isInNet(ia.getHostAddress())) { + return true; + } + } + } catch (UnknownHostException e) { + // It's ok. do normal Verify + } + return origHV.verify(urlHostName, session); + }; + }; + HttpsURLConnection.setDefaultHostnameVerifier(maskHV); + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/SecurityInfoC.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/SecurityInfoC.java new file mode 100644 index 00000000..1d8ced55 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/SecurityInfoC.java @@ -0,0 +1,94 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.config; + +import java.net.HttpURLConnection; +import java.util.HashMap; +import java.util.Map; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.SecuritySetter; + + +public class SecurityInfoC<CLIENT> extends SecurityInfo { + public static final String DEF_ID = "ID not Set"; + private static Map<Class<?>,SecurityInfoC<?>> sicMap = new HashMap<>(); + public SecuritySetter<CLIENT> defSS; + + + public SecurityInfoC(Access access) throws CadiException { + super(access); + defSS = new DEFSS<CLIENT>(); + } + + @SuppressWarnings("unchecked") + public static synchronized <CLIENT> SecurityInfoC<CLIENT> instance(Access access, Class<CLIENT> cls) throws CadiException { + SecurityInfoInit<CLIENT> sii; + if (cls.isAssignableFrom(HttpURLConnection.class)) { + try { + @SuppressWarnings("rawtypes") + Class<SecurityInfoInit> initCls = (Class<SecurityInfoInit>)Class.forName("org.onap.ccsdk.apps.cadi.http.HSecurityInfoInit"); + sii = initCls.newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + throw new CadiException("CADI using HttpURLConnection requires cadi-client jar",e); + } + } else { + sii = new SecurityInfoInit<CLIENT>() { + @Override + public SecuritySetter<CLIENT> bestDefault(SecurityInfoC<CLIENT> si) throws CadiException { + return new DEFSS<CLIENT>(); + } + }; + } + + SecurityInfoC<CLIENT> sic = (SecurityInfoC<CLIENT>) sicMap.get(cls); + if (sic==null) { + sic = new SecurityInfoC<CLIENT>(access); + sic.set(sii.bestDefault(sic)); + sicMap.put(cls, sic); + } + return sic; + } + + public SecurityInfoC<CLIENT> set(SecuritySetter<CLIENT> defSS) { + this.defSS = defSS; + return this; + } + + private static class DEFSS<C> implements SecuritySetter<C> { + @Override + public String getID() { + return DEF_ID; + } + + @Override + public void setSecurity(C client) throws CadiException { + throw new CadiException("No Client Credentials set."); + } + + @Override + public int setLastResponse(int respCode) { + return 0; + } + }; +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/SecurityInfoInit.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/SecurityInfoInit.java new file mode 100644 index 00000000..88229b8e --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/SecurityInfoInit.java @@ -0,0 +1,28 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ +package org.onap.ccsdk.apps.cadi.config; + +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.SecuritySetter; + +public interface SecurityInfoInit<CLIENT> { + public SecuritySetter<CLIENT> bestDefault(SecurityInfoC<CLIENT> si) throws CadiException; +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/UsersDump.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/UsersDump.java new file mode 100644 index 00000000..6fc83f5f --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/config/UsersDump.java @@ -0,0 +1,162 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.config; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Date; +import java.util.HashSet; + +import org.onap.ccsdk.apps.cadi.AbsUserCache; +import org.onap.ccsdk.apps.cadi.lur.LocalLur; + +public class UsersDump { + + /** + * @param args + */ + public static boolean write(OutputStream os, AbsUserCache<?> lur) { + PrintStream ps; + if (os instanceof PrintStream) { + ps = (PrintStream)os; + } else { + ps = new PrintStream(os); + } + try { + ps.println("<?xml version='1.0' encoding='utf-8'?>"); + ps.println("<!--"); + ps.print( " Code Generated Tomcat Users and Roles from AT&T LUR on "); + ps.println(new Date()); + ps.println( "-->"); + ps.println("<tomcat-users>"); + + // We loop through Users, but want to write Groups first... therefore, save off print + StringBuilder sb = new StringBuilder(); + + // Obtain all unique role names + HashSet<String> groups = new HashSet<>(); + for (AbsUserCache<?>.DumpInfo di : lur.dumpInfo()) { + sb.append("\n <user username=\""); + sb.append(di.user); + sb.append("\" roles=\""); + boolean first = true; + for (String role : di.perms) { + groups.add(role); + if (first)first = false; + else sb.append(','); + sb.append(role); + } + sb.append("\"/>"); + + } + + // Print roles + for (String group : groups) { + ps.print(" <role rolename=\""); + ps.print(group); + ps.println("\"/>"); + } + + ps.println(sb); + + ps.println("</tomcat-users>"); + ps.flush(); + } catch (Exception t) { + t.printStackTrace(ps); + return false; + } + return true; + } + + /** + * + * Note: This method returns a String if there's an error, or null if ok. + * This unusual style is necessitated by the fact that any Exceptions thrown are likely to + * be unlogged and hidden from view, making debugging almost impossible. + * + * @param writeto + * @param up + * @return + */ + public static String updateUsers(String writeto, LocalLur up) { + // Dump a Tomcat-user.xml lookalike (anywhere) + if (writeto!=null) { + // First read content + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + if (UsersDump.write(baos, up)) { + byte[] postulate = baos.toByteArray(); + // now get contents of file + File file = new File(writeto); + boolean writeIt; + if (file.exists()) { + try { + FileInputStream fis = new FileInputStream(file); + byte[] orig = new byte[(int)file.length()]; + int read; + try { + read = fis.read(orig); + } finally { + fis.close(); + } + if (read<=0) { + writeIt = false; + } else { + // Starting at third "<" (<tomcat-users> line) + int startA=0, startB=0; + for (int i=0;startA<orig.length && i<3;++startA) if (orig[startA]=='<')++i; + for (int i=0;startB<orig.length && i<3;++startB) if (postulate[startB]=='<')++i; + + writeIt=orig.length-startA!=postulate.length-startB; // first, check if remaining length is the same + while (!writeIt && startA<orig.length && startB<postulate.length) { + if (orig[startA++]!=postulate[startB++])writeIt = true; + } + } + } catch (Exception e) { + writeIt = true; + } + } else { + writeIt = true; + } + + if (writeIt) { + try { + FileOutputStream fos = new FileOutputStream(file); + try { + fos.write(postulate); + } finally { + fos.close(); + } + } catch (IOException e) { + return e.getMessage(); + } + } + } + } + return null; // no message means ok. + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/AUTHZ.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/AUTHZ.java new file mode 100644 index 00000000..3c18f8df --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/AUTHZ.java @@ -0,0 +1,36 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.filter; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.servlet.Servlet; + +@Target({TYPE}) +@Retention(RUNTIME) +public @interface AUTHZ { + Class<? extends Servlet> value(); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/AUTHZServlet.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/AUTHZServlet.java new file mode 100644 index 00000000..952c11ec --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/AUTHZServlet.java @@ -0,0 +1,98 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.filter; + +import java.io.IOException; + +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * + * @author Jonathan + * + */ +public class AUTHZServlet<S extends Servlet> implements Servlet { + private String[] roles; + private Servlet delegate; + + protected AUTHZServlet(Class<S> cls) { + try { + delegate = cls.newInstance(); + } catch (Exception e) { + delegate = null; + } + RolesAllowed rolesAllowed = cls.getAnnotation(RolesAllowed.class); + if (rolesAllowed == null) { + roles = null; + } else { + roles = rolesAllowed.value(); + } + } + + public void init(ServletConfig sc) throws ServletException { + if (delegate == null) { + throw new ServletException("Invalid Servlet Delegate"); + } + delegate.init(sc); + } + + public ServletConfig getServletConfig() { + return delegate.getServletConfig(); + } + + public String getServletInfo() { + return delegate.getServletInfo(); + } + + public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException { + if (roles == null) { + delegate.service(req, resp); + return; + } + + // Validate + try { + HttpServletRequest hreq = (HttpServletRequest)req; + for (String role : roles) { + if (hreq.isUserInRole(role)) { + delegate.service(req, resp); + return; + } + } + + ((HttpServletResponse)resp).sendError(403); // forbidden + } catch (ClassCastException e) { + throw new ServletException("JASPIServlet only supports HTTPServletRequest/HttpServletResponse"); + } + } + + public void destroy() { + delegate.destroy(); + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/AccessGetter.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/AccessGetter.java new file mode 100644 index 00000000..36370c4e --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/AccessGetter.java @@ -0,0 +1,35 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ +package org.onap.ccsdk.apps.cadi.filter; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.config.Get; + +public class AccessGetter implements Get { + private final Access access; + public AccessGetter(Access access) { + this.access = access; + } + public String get(String name, String def, boolean print) { + return access.getProperty(name, def); + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/CadiApiEnforcementFilter.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/CadiApiEnforcementFilter.java new file mode 100644 index 00000000..2e376419 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/CadiApiEnforcementFilter.java @@ -0,0 +1,136 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + */ +package org.onap.ccsdk.apps.cadi.filter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.ServletContextAccess; +import org.onap.ccsdk.apps.cadi.config.Config; +import org.onap.ccsdk.apps.cadi.util.Split; + +/** + * This filter allows one to protect the APIs from data stored in AAF + * + * @author Instrumental(Jonathan) + */ +public class CadiApiEnforcementFilter implements Filter { + private String type; + private Map<String,List<String>> publicPaths; + private Access access; + + + public CadiApiEnforcementFilter(Access access, String enforce) throws ServletException { + this.access = access; + init(enforce); + } + + + @Override + public void init(FilterConfig fc) throws ServletException { + init(fc.getInitParameter(Config.CADI_API_ENFORCEMENT)); + // need the Context for Logging, instantiating ClassLoader, etc + ServletContextAccess sca=new ServletContextAccess(fc); + if (access==null) { + access = sca; + } + } + + private void init(final String ptypes) throws ServletException { + if(ptypes==null) { + throw new ServletException("CadiApiEnforcement requires " + Config.CADI_API_ENFORCEMENT + " property"); + } + String[] full = Split.splitTrim(';', ptypes); + if(full.length==0) { + throw new ServletException(Config.CADI_API_ENFORCEMENT + " property is empty"); + } + if(full.length>0) { + type=full[0]; + } + publicPaths = new TreeMap<String,List<String>>(); + if(full.length>1) { + for(int i=1;i<full.length;++i) { + String pubArray[] = Split.split(':', full[i]); + if(pubArray.length==2) { + List<String> ls = publicPaths.get(pubArray[0]); + if(ls==null) { + ls = new ArrayList<String>(); + publicPaths.put(pubArray[0], ls); + } + ls.add(pubArray[1]); + } + } + } + } + + + @Override + public void doFilter(ServletRequest req, ServletResponse resp, FilterChain fc) throws IOException, ServletException { + HttpServletRequest hreq = (HttpServletRequest)req; + final String meth = hreq.getMethod(); + String path = hreq.getContextPath()+hreq.getPathInfo(); + + if(path == null || path.isEmpty() || "null".equals(path)) + path = hreq.getRequestURI().substring(hreq.getContextPath().length()); + + List<String> list = publicPaths.get(meth); + if(list!=null) { + for( String p : publicPaths.get(meth)) { + if(path.startsWith(p)) { + access.printf(Level.INFO, "%s accessed public API %s %s\n", + hreq.getUserPrincipal().getName(), + meth, + path); + fc.doFilter(req, resp); + return; + } + } + } + if(hreq.isUserInRole(type + '|'+path+'|'+meth)) { + access.printf(Level.INFO, "%s is allowed access to %s %s\n", + hreq.getUserPrincipal().getName(), + meth, + path); + fc.doFilter(req, resp); + } else { + access.printf(Level.AUDIT, "%s is denied access to %s %s\n", + hreq.getUserPrincipal().getName(), + meth, + path); + } + } + + @Override + public void destroy() { + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/CadiFilter.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/CadiFilter.java new file mode 100644 index 00000000..35d59d52 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/CadiFilter.java @@ -0,0 +1,358 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.filter; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.CadiWrap; +import org.onap.ccsdk.apps.cadi.LocatorException; +import org.onap.ccsdk.apps.cadi.Lur; +import org.onap.ccsdk.apps.cadi.PropAccess; +import org.onap.ccsdk.apps.cadi.ServletContextAccess; +import org.onap.ccsdk.apps.cadi.TrustChecker; +import org.onap.ccsdk.apps.cadi.config.Config; +import org.onap.ccsdk.apps.cadi.config.Get; +import org.onap.ccsdk.apps.cadi.taf.TafResp; +import org.onap.ccsdk.apps.cadi.taf.TafResp.RESP; +import org.onap.ccsdk.apps.cadi.util.Timing; + +/** + * CadiFilter + * + * This class implements Servlet Filter, and ties together CADI implementations + * + * This class can be used in a standard J2EE Servlet manner. Optimal usage is for POJO operations, where + * one can enforce this Filter being first and primary. Depending on the Container, it + * may be more effective, in some cases, to utilize features that allow earlier determination of + * AUTHN (Authorization). An example would be "Tomcat Valve". These implementations, however, should + * be modeled after the "init" and "doFilter" functions, and be kept up to date as this class changes. + * + * + * @author Jonathan + * + */ +public class CadiFilter implements Filter { + private static CadiHTTPManip httpChecker; + private static String[] pathExceptions; + private static List<Pair> mapPairs; + private Access access; + private Object[] additionalTafLurs; + private SideChain sideChain; + private static int count=0; + + public Lur getLur() { + return httpChecker.getLur(); + } + + /** + * Construct a viable Filter + * + * Due to the vagaries of many containers, there is a tendency to create Objects and call "Init" on + * them at a later time. Therefore, this object creates with an object that denies all access + * until appropriate Init happens, just in case the container lets something slip by in the meantime. + * + */ + public CadiFilter() { + additionalTafLurs = CadiHTTPManip.noAdditional; + } + + /** + * This constructor to be used when directly constructing and placing in HTTP Engine + * + * @param access + * @param moreTafLurs + * @throws ServletException + */ + public CadiFilter(Access access, Object ... moreTafLurs) throws ServletException { + additionalTafLurs = moreTafLurs; + init(new AccessGetter(this.access = access)); + } + + + /** + * Use this to pass in a PreContructed CADI Filter, but with initializing... let Servlet do it + * @param init + * @param access + * @param moreTafLurs + * @throws ServletException + */ + public CadiFilter(boolean init, PropAccess access, Object ... moreTafLurs) throws ServletException { + this.access = access; + additionalTafLurs = moreTafLurs; + if (init) { + init(new AccessGetter(access)); + } + } + + /** + * Init + * + * Standard Filter "init" call with FilterConfig to obtain properties. POJOs can construct a + * FilterConfig with the mechanism of their choice, and standard J2EE Servlet engines utilize this + * mechanism already. + */ + //TODO Always validate changes against Tomcat AbsCadiValve and Jaspi CadiSAM Init functions + public void init(FilterConfig filterConfig) throws ServletException { + // need the Context for Logging, instantiating ClassLoader, etc + ServletContextAccess sca=new ServletContextAccess(filterConfig); + if (access==null) { + access = sca; + } + + // Set Protected getter with base Access, for internal class instantiations + init(new FCGet(access, sca.context(), filterConfig)); + } + + + @SuppressWarnings("unchecked") + protected void init(Get getter) throws ServletException { + sideChain = new SideChain(); + // Start with the assumption of "Don't trust anyone". + TrustChecker tc = TrustChecker.NOTRUST; // default position + try { + Class<TrustChecker> ctc = (Class<TrustChecker>) Class.forName("org.onap.ccsdk.apps.cadi.aaf.v2_0.AAFTrustChecker"); + if (ctc!=null) { + Constructor<TrustChecker> contc = ctc.getConstructor(Access.class); + if (contc!=null) { + tc = contc.newInstance(access); + } + } + } catch (Exception e) { + access.log(Level.INIT, "AAFTrustChecker cannot be loaded",e.getMessage()); + } + + try { + Class<Filter> cf=null; + try { + cf= (Class<Filter>) Class.forName("org.onap.ccsdk.apps.cadi.oauth.OAuthFilter"); + sideChain.add(cf.newInstance()); + } catch (ClassNotFoundException e) { + access.log(Level.DEBUG, "OAuthFilter not enabled"); + } + } catch (Exception e) { + access.log(Level.INIT, "AAFTrustChecker cannot be loaded",e.getMessage()); + } + + + // Synchronize, because some instantiations call init several times on the same object + // In this case, the epiTaf will be changed to a non-NullTaf, and thus not instantiate twice. + synchronized(CadiHTTPManip.noAdditional /*will always remain same Object*/) { + ++count; + if (httpChecker == null) { + if (access==null) { + access = new PropAccess(); + } + try { + httpChecker = new CadiHTTPManip(access,null /*reuseable Con*/,tc, additionalTafLurs); + } catch (CadiException | LocatorException e1) { + throw new ServletException(e1); + } + } else if (access==null) { + access= httpChecker.getAccess(); + } + + /* + * Setup Authn Path Exceptions + */ + if (pathExceptions==null) { + String str = getter.get(Config.CADI_NOAUTHN, null, true); + if (str!=null) { + pathExceptions = str.split("\\s*:\\s*"); + } + } + + /* + * SETUP Permission Converters... those that can take Strings from a Vendor Product, and convert to appropriate AAF Permissions + */ + if (mapPairs==null) { + String str = getter.get(Config.AAF_PERM_MAP, null, true); + if (str!=null) { + String mstr = getter.get(Config.AAF_PERM_MAP, null, true); + if (mstr!=null) { + String map[] = mstr.split("\\s*:\\s*"); + if (map.length>0) { + MapPermConverter mpc=null; + int idx; + mapPairs = new ArrayList<>(); + for (String entry : map) { + if ((idx=entry.indexOf('='))<0) { // it's a Path, so create a new converter + access.log(Level.INIT,"Loading Perm Conversions for:",entry); + mapPairs.add(new Pair(entry,mpc=new MapPermConverter())); + } else { + if (mpc!=null) { + mpc.map().put(entry.substring(0,idx),entry.substring(idx+1)); + } else { + access.log(Level.ERROR,"cadi_perm_map is malformed; ",entry, "is skipped"); + } + } + } + } + } + } + } + } + + // Add API Enforcement Point + String enforce = getter.get(Config.CADI_API_ENFORCEMENT, null, true); + if(enforce!=null && enforce.length()>0) { + sideChain.add(new CadiApiEnforcementFilter(access,enforce)); + } + // Remove Getter + getter = Get.NULL; + } + + /** + * Containers call "destroy" when time to cleanup + */ + public void destroy() { + // Synchronize, in case multiCadiFilters are used. + synchronized(CadiHTTPManip.noAdditional) { + if (--count<=0 && httpChecker!=null) { + httpChecker.destroy(); + httpChecker=null; + access=null; + pathExceptions=null; + } + } + } + + /** + * doFilter + * + * This is the standard J2EE invocation. Analyze the request, modify response as necessary, and + * only call the next item in the filterChain if request is suitably Authenticated. + */ + //TODO Always validate changes against Tomcat AbsCadiValve and Jaspi CadiSAM functions + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + final long startAll = System.nanoTime(); + long startCode, startValidate; + float code=0f, validate=0f; + String user = "n/a"; + String tag = ""; + TafResp tresp = null; + try { + HttpServletRequest hreq = (HttpServletRequest)request; + if (noAuthn(hreq)) { + startCode=System.nanoTime(); + chain.doFilter(request, response); + code = Timing.millis(startCode); + } else { + HttpServletResponse hresp = (HttpServletResponse)response; + startValidate=System.nanoTime(); + tresp = httpChecker.validate(hreq, hresp, hreq); + validate = Timing.millis(startValidate); + if (tresp.isAuthenticated()==RESP.IS_AUTHENTICATED) { + user = tresp.getPrincipal().personalName(); + tag = tresp.getPrincipal().tag(); + CadiWrap cw = new CadiWrap(hreq, tresp, httpChecker.getLur(),getConverter(hreq)); + if (httpChecker.notCadi(cw, hresp)) { + startCode=System.nanoTime(); + sideChain.doFilter(cw,response,chain); + code = Timing.millis(startCode); + } + } + } + } catch (ClassCastException e) { + throw new ServletException("CadiFilter expects Servlet to be an HTTP Servlet",e); + } finally { + if (tresp != null) { + access.printf(Level.INFO, "Trans: user=%s[%s],ip=%s,ms=%f,validate=%f,code=%f,result=%s", + user,tag,request.getRemoteAddr(), + Timing.millis(startAll),validate,code,tresp.isAuthenticated().toString()); + } else { + access.printf(Level.INFO, "Trans: user=%s[%s],ip=%s,ms=%f,validate=%f,code=%f,result=FAIL", + user,tag,request.getRemoteAddr(), + Timing.millis(startAll),validate,code); + } + } + } + + + /** + * If PathExceptions exist, report if these should not have Authn applied. + * @param hreq + * @return + */ + private boolean noAuthn(HttpServletRequest hreq) { + if (pathExceptions!=null) { + String pi = hreq.getPathInfo(); + if (pi==null) { + // Attempt to get from URI only (Daniel Rose) + pi = hreq.getRequestURI().substring(hreq.getContextPath().length()); + if(pi==null) { + // Nothing works. + return false; // JBoss sometimes leaves null + } + } + for (String pe : pathExceptions) { + if (pi.startsWith(pe))return true; + } + } + return false; + } + + /** + * Get Converter by Path + */ + private PermConverter getConverter(HttpServletRequest hreq) { + if (mapPairs!=null) { + String pi = hreq.getPathInfo(); + if (pi !=null) { + for (Pair p: mapPairs) { + if (pi.startsWith(p.name))return p.pc; + } + } + } + return NullPermConverter.singleton(); + } + + /** + * store PermConverters by Path prefix + * @author Jonathan + * + */ + private class Pair { + public Pair(String key, PermConverter pc) { + name = key; + this.pc = pc; + } + public String name; + public PermConverter pc; + } + +} + diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/CadiHTTPManip.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/CadiHTTPManip.java new file mode 100644 index 00000000..91760aff --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/CadiHTTPManip.java @@ -0,0 +1,221 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.filter; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.CadiWrap; +import org.onap.ccsdk.apps.cadi.Connector; +import org.onap.ccsdk.apps.cadi.CredVal; +import org.onap.ccsdk.apps.cadi.LocatorException; +import org.onap.ccsdk.apps.cadi.Lur; +import org.onap.ccsdk.apps.cadi.Taf; +import org.onap.ccsdk.apps.cadi.TrustChecker; +import org.onap.ccsdk.apps.cadi.config.Config; +import org.onap.ccsdk.apps.cadi.config.SecurityInfoC; +import org.onap.ccsdk.apps.cadi.lur.EpiLur; +import org.onap.ccsdk.apps.cadi.taf.HttpTaf; +import org.onap.ccsdk.apps.cadi.taf.TafResp; +import org.onap.ccsdk.apps.cadi.util.UserChainManip; + +/** + * Encapsulate common HTTP Manipulation Behavior. It will appropriately set + * HTTPServletResponse for Redirect or Forbidden, as needed. + * + * Further, this is useful, because it avoids multiple creates of Connections, where some Filters + * are created and destroyed regularly. + * + * @author Jonathan + * + */ +public class CadiHTTPManip { + private static final String ACCESS_DENIED = "Access Denied"; + private static final String NO_TAF_WILL_AUTHORIZE = "No TAF will authorize"; + private static final String AUTHENTICATION_FAILURE = "Authentication Failure"; + private static final String AUTHENTICATING_VIA_REDIRECTION = "Authenticating via redirection"; + private static final String MSG_FMT = "user=%s,ip=%s:%d,msg=\"%s: %s\""; + private static final String AUTHENTICATED = "Authenticated"; + private static final String ACCESS_CADI_CONTROL = ".access|cadi|control"; + private static final String METH = "OPTIONS"; + private static final String CADI = "/cadi/"; + private static final String CADI_CACHE_PRINT = "/cadi/cache/print"; + private static final String CADI_CACHE_CLEAR = "/cadi/cache/clear"; + private static final String CADI_LOG_SET = "/cadi/log/set/"; + private static final Object LOCK = new Object(); + private Access access; + private HttpTaf taf; + private CredVal up; + private Lur lur; + private String thisPerm,companyPerm,aaf_id; + + public static final Object[] noAdditional = new Object[0]; // CadiFilter can be created each call in some systems + + + public CadiHTTPManip(Access access, Connector con, TrustChecker tc, Object ... additionalTafLurs) throws CadiException, LocatorException { + synchronized(LOCK) { + this.access = access; +// Get getter = new AccessGetter(access); + Config.setDefaultRealm(access); + + aaf_id = access.getProperty(Config.CADI_ALIAS,access.getProperty(Config.AAF_APPID, null)); + if (aaf_id==null) { + access.printf(Level.INIT, "%s is not set. %s can be used instead",Config.AAF_APPID,Config.CADI_ALIAS); + } else { + access.printf(Level.INIT, "%s is set to %s",Config.AAF_APPID,aaf_id); + } + String ns = aaf_id==null?null:UserChainManip.idToNS(aaf_id); + if (ns!=null) { + thisPerm = ns+ACCESS_CADI_CONTROL; + int dot = ns.indexOf('.'); + if (dot>=0) { + int dot2=ns.indexOf('.',dot+1); + if (dot2<0) { + dot2=dot; + } + companyPerm = ns.substring(0, dot2)+ACCESS_CADI_CONTROL; + } else { + companyPerm = "com"+ACCESS_CADI_CONTROL; + } + } else { + thisPerm = companyPerm = "com"+ACCESS_CADI_CONTROL; + } + SecurityInfoC<HttpURLConnection> si; + si = SecurityInfoC.instance(access, HttpURLConnection.class); + + lur = Config.configLur(si, con, additionalTafLurs); + + tc.setLur(lur); + if (lur instanceof EpiLur) { + up = ((EpiLur)lur).getUserPassImpl(); + } else if (lur instanceof CredVal) { + up = (CredVal)lur; + } else { + up = null; + } + taf = Config.configHttpTaf(con,si, tc, up, lur, additionalTafLurs); + } + } + + public TafResp validate(HttpServletRequest hreq, HttpServletResponse hresp, Object state) throws IOException { + TafResp tresp = taf.validate(Taf.LifeForm.LFN, hreq, hresp); + switch(tresp.isAuthenticated()) { + case IS_AUTHENTICATED: + access.printf(Level.DEBUG,MSG_FMT,tresp.getTarget(),hreq.getRemoteAddr(), + hreq.getRemotePort(),AUTHENTICATED,tresp.desc()); + break; + case TRY_AUTHENTICATING: + switch (tresp.authenticate()) { + case IS_AUTHENTICATED: + access.printf(Level.DEBUG,MSG_FMT,tresp.getTarget(),hreq.getRemoteAddr(), + hreq.getRemotePort(),AUTHENTICATED,tresp.desc()); + break; + case HTTP_REDIRECT_INVOKED: + access.printf(Level.DEBUG,MSG_FMT,tresp.getTarget(),hreq.getRemoteAddr(), + hreq.getRemotePort(),AUTHENTICATING_VIA_REDIRECTION,tresp.desc()); + break; + case NO_FURTHER_PROCESSING: + access.printf(Level.AUDIT,MSG_FMT,tresp.getTarget(),hreq.getRemoteAddr(), + hreq.getRemotePort(),AUTHENTICATION_FAILURE,tresp.desc()); + hresp.sendError(403, tresp.desc()); // Forbidden + break; + + default: + access.printf(Level.AUDIT,MSG_FMT,tresp.getTarget(),hreq.getRemoteAddr(), + hreq.getRemotePort(),NO_TAF_WILL_AUTHORIZE,tresp.desc()); + hresp.sendError(403, tresp.desc()); // Forbidden + } + break; + case NO_FURTHER_PROCESSING: + access.printf(Level.AUDIT,MSG_FMT, tresp.getTarget(),hreq.getRemoteAddr(), + hreq.getRemotePort(),NO_TAF_WILL_AUTHORIZE,tresp.desc()); + hresp.sendError(403, ACCESS_DENIED); // FORBIDDEN + break; + default: + access.printf(Level.AUDIT,MSG_FMT, tresp.getTarget(),hreq.getRemoteAddr(), + hreq.getRemotePort(),NO_TAF_WILL_AUTHORIZE,tresp.desc()); + hresp.sendError(403, ACCESS_DENIED); // FORBIDDEN + } + + return tresp; + } + + public boolean notCadi(CadiWrap req, HttpServletResponse resp) { + + String pathInfo = req.getPathInfo(); + if (METH.equalsIgnoreCase(req.getMethod()) && pathInfo!=null && pathInfo.contains(CADI)) { + if (req.getUser().equals(aaf_id) || req.isUserInRole(thisPerm) || req.isUserInRole(companyPerm)) { + try { + if (pathInfo.contains(CADI_CACHE_PRINT)) { + resp.getOutputStream().println(lur.toString()); + resp.setStatus(200); + return false; + } else if (pathInfo.contains(CADI_CACHE_CLEAR)) { + StringBuilder report = new StringBuilder(); + lur.clear(req.getUserPrincipal(), report); + resp.getOutputStream().println(report.toString()); + resp.setStatus(200); + return false; + } else if (pathInfo.contains(CADI_LOG_SET)) { + Level l; + int slash = pathInfo.lastIndexOf('/'); + String level = pathInfo.substring(slash+1); + try { + l = Level.valueOf(level); + access.printf(Level.AUDIT, "%s has set CADI Log Level to '%s'",req.getUser(),l.name()); + access.setLogLevel(l); + } catch (IllegalArgumentException e) { + access.printf(Level.AUDIT, "'%s' is not a valid CADI Log Level",level); + } + return false; + } + } catch (IOException e) { + access.log(e); + } + } + } + return true; + } + + public Lur getLur() { + return lur; + } + + public void destroy() { + access.log(Level.INFO,"CadiHttpChecker destroyed."); + if (lur!=null) { + lur.destroy(); + lur=null; + } + } + + public Access getAccess() { + return access; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/FCGet.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/FCGet.java new file mode 100644 index 00000000..d51954f5 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/FCGet.java @@ -0,0 +1,76 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.filter; + +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.config.Get; + +/* + * A private method to query the Filter config and if not exists, return the default. This + * cleans up the initialization code. + */ +public class FCGet implements Get { + /** + * + */ + private final Access access; + private FilterConfig filterConfig; + private ServletContext context; + + public FCGet(Access access, ServletContext context, FilterConfig filterConfig) { + this.access = access; + this.context = context; + this.filterConfig = filterConfig; + } + + public String get(String name, String def, boolean print) { + String str = null; + // Try Server Context First + if (context!=null) { + str = context.getInitParameter(name); + } + + // Try Filter Context next + if (str==null && filterConfig != null) { + str = filterConfig.getInitParameter(name); + } + + if (str==null) { + str = access.getProperty(name, def); + } + // Take def if nothing else + if (str==null) { + str = def; + // don't log defaults + } else { + str = str.trim(); // this is vital in Property File based values, as spaces can hide easily + if (print) { + access.log(Level.INFO,"Setting", name, "to", str); + } + } + return str; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/MapBathConverter.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/MapBathConverter.java new file mode 100644 index 00000000..1b131882 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/MapBathConverter.java @@ -0,0 +1,175 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.filter; + +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.Symm; +import org.onap.ccsdk.apps.cadi.util.CSV; +import org.onap.ccsdk.apps.cadi.util.CSV.Visitor; +import org.onap.ccsdk.apps.cadi.util.Holder; + +/** + * This Filter is designed to help MIGRATE users from systems that don't match the FQI style. + * + * Style 1, where just the ID is translated, i.e. OLD => new@something.onap.org, that is acceptable + * longer term, because it does not store Creds locally. The passwords are in appropriate systems, but + * it's still painful operationally, though it does ease migration. + * + * Style 3, however, which is Direct match of Authorization Header to replacement, is only there + * because some passwords are simply not acceptable for AAF, (too easy, for instance), and it is + * not feasible to break Organization Password rules for a Migration. Therefore, this method + * should not considered something that is in any way a permanent + * + + * + * It goes without saying that any file with the password conversion should be protected by "400", etc. + * + * @author Instrumental (Jonathan) + * + */ +public class MapBathConverter { + private static final String BASIC = "Basic "; + private final Map<String,String> map; + + /** + * Create with colon separated name value pairs + * Enter the entire "Basic dXNlcjpwYXNz" "Authorization" header, where "dXNlcjpwYXNz" is + * base64 encoded, which can be created with "cadi" tool (in jar) + * + * The replacement should also be an exact replacement of what you want. Recognize that + * this should be TEMPORARY as you are storing credentials outside the users control. + * + * @param value + * @throws IOException + * @throws CadiException + */ + public MapBathConverter(final Access access, final CSV csv) throws IOException, CadiException { + map = new TreeMap<>(); + final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + final Date now = new Date(); + csv.visit(new Visitor() { + @Override + public void visit(List<String> row) throws CadiException { + if(row.size()<3) { + throw new CadiException("CSV file " + csv + " must have at least 2 Basic Auth columns and an Expiration Date(YYYY-MM-DD) in each row"); + } + try { + Date date = sdf.parse(row.get(2)); + String oldID = row.get(0); + String newID = row.get(1); + if(date.after(now)) { + if(!oldID.startsWith(BASIC) && newID.startsWith(BASIC)) { + throw new CadiException("CSV file " + csv + ": Uncredentialed ID " + idFromBasic(oldID,null) + + " may not transfer to credentialed ID " + idFromBasic(newID,null)); + } else { + map.put(oldID,newID); + access.printf(Level.INIT, "ID Conversion from %s to %s enabled", + idFromBasic(oldID,null), + idFromBasic(newID,null)); + } + } else { + access.printf(Level.INIT, "ID Conversion from %s to %s has expired.", + idFromBasic(oldID,null), + idFromBasic(newID,null)); + } + } catch (ParseException e) { + throw new CadiException("Cannot Parse Date: " + row.get(2)); + } catch (IOException e) { + throw new CadiException(e); + } + } + }); + } + + private static String idFromBasic(String bath, Holder<String> hpass) throws IOException, CadiException { + if(bath.startsWith(BASIC)) { + String cred = Symm.base64noSplit.decode(bath.substring(6)); + int colon = cred.indexOf(':'); + if(colon<0) { + throw new CadiException("Invalid Authentication Credential for " + cred); + } + if(hpass!=null) { + hpass.set(cred.substring(colon+1)); + } + return cred.substring(0, colon); + } else { + return bath; + } + } + + /** + * use to instantiate entries + * + * @return + */ + public Map<String,String> map() { + return map; + } + + public String convert(Access access, final String bath) { + String rv = map.get(bath); + + String cred; + String tcred=null; + Holder<String> hpass=null; + try { + if(bath.startsWith(BASIC)) { + cred = idFromBasic(bath,(hpass=new Holder<String>(null))); + if(rv==null) { + rv = map.get(cred); + } + } else { + cred = bath; + } + + if(rv==null) { + // Nothing here, just return original + rv = bath; + } else { + if(rv.startsWith(BASIC)) { + tcred = idFromBasic(rv,null); + } else { + if(hpass!=null) { + tcred = rv; + rv = BASIC + Symm.base64noSplit.encode(rv+':'+hpass.get()); + } + } + if(tcred != null) { + access.printf(Level.AUDIT, "ID %s converted to %s",cred,tcred); + } + } + } catch (IOException | CadiException e) { + access.log(e,"Invalid Authorization"); + } + return rv==null?bath:rv; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/MapPermConverter.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/MapPermConverter.java new file mode 100644 index 00000000..72e20cb8 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/MapPermConverter.java @@ -0,0 +1,54 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.filter; + +import java.util.HashMap; +import java.util.Map; + +public class MapPermConverter implements PermConverter { + private HashMap<String,String> map; + + /** + * Create with colon separated name value pairs + * i.e. teAdmin=com.att.myNS.myPerm|*|*:teUser=... + * + * @param value + */ + public MapPermConverter() { + map = new HashMap<>(); + } + + /** + * use to instantiate entries + * + * @return + */ + public Map<String,String> map() { + return map; + } + + public String convert(String minimal) { + String rv = map.get(minimal); + return (rv == null) ? minimal : rv; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/NullPermConverter.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/NullPermConverter.java new file mode 100644 index 00000000..6ddd1f32 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/NullPermConverter.java @@ -0,0 +1,45 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.filter; + + +/** + * A NullPermConverter + * + * Obey the PermConverter Interface, but passed in "minimal" String is not converted. + * + * @author Jonathan + * + */ +public class NullPermConverter implements PermConverter { + + private static final NullPermConverter singleton = new NullPermConverter(); + + private NullPermConverter() {} + + public static NullPermConverter singleton() { return singleton; } + + public String convert(String minimal) { + return minimal; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/PathFilter.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/PathFilter.java new file mode 100644 index 00000000..458a4440 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/PathFilter.java @@ -0,0 +1,180 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.filter; + +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.config.Config; + +/** + * PathFilter + * + * This class implements Servlet Filter, and uses AAF to validate access to a Path. + * + * This class can be used in a standard J2EE Servlet manner. + * + * @author Jonathan, collaborating with Xue Gao + * + */ +public class PathFilter implements Filter { + private final Log log; + + private ServletContext context; + private String aafType; + private String notAuthorizedMsg; + + /** + * Construct a viable Filter for installing in Container WEB.XML, etc. + * + */ + public PathFilter() { + log = new Log() { + public void info(String ... msg) { + context.log(build("INFO:", msg)); + } + public void audit(String ... msg) { + context.log(build("AUDIT:", msg)); + } + private String build(String type, String []msg) { + StringBuilder sb = new StringBuilder(type); + for (String s : msg) { + sb.append(' '); + sb.append(s); + } + return sb.toString(); + } + }; + } + + /** + * Filter that can be constructed within Java + * @param access + */ + public PathFilter(final Access access) { + log = new Log() { + public void info(String ... msg) { + access.log(Level.INFO, (Object[])msg); + } + public void audit(String ... msg) { + access.log(Level.AUDIT, (Object[])msg); + } + }; + } + + /** + * Init + * + * Standard Filter "init" call with FilterConfig to obtain properties. POJOs can construct a + * FilterConfig with the mechanism of their choice, and standard J2EE Servlet engines utilize this + * mechanism already. + */ + public void init(FilterConfig filterConfig) throws ServletException { + // need the Context for Logging, instantiating ClassLoader, etc + context = filterConfig.getServletContext(); + StringBuilder sb = new StringBuilder(); + StringBuilder err = new StringBuilder(); + Object attr = context.getAttribute(Config.PATHFILTER_NS); + if (attr == null) { + err.append("PathFilter - pathfilter_ns is not set"); + } else { + sb.append(attr.toString()); + } + + attr = context.getAttribute(Config.PATHFILTER_STACK); + if (attr == null) { + log.info("PathFilter - No pathfilter_stack set, ignoring"); + } else { + sb.append('.'); + sb.append(attr.toString()); + } + + attr = context.getAttribute(Config.PATHFILTER_URLPATTERN); + if (attr == null) { + log.info("PathFilter - No pathfilter_urlpattern set, defaulting to 'urlpattern'"); + sb.append(".urlpattern"); + } else { + sb.append('.'); + sb.append(attr.toString()); + } + + log.info("PathFilter - AAF Permission Type is", sb.toString()); + + sb.append('|'); + + aafType = sb.toString(); + + attr = context.getAttribute(Config.PATHFILTER_NOT_AUTHORIZED_MSG); + if (attr == null) { + notAuthorizedMsg = "Forbidden - Not Authorized to access this Path"; + } else { + notAuthorizedMsg = attr.toString(); + } + + if (err.length() > 0) { + throw new ServletException(err.toString()); + } + } + + private interface Log { + public void info(String ... msg); + public void audit(String ... msg); + } + + /** + * doFilter + * + * This is the standard J2EE invocation. Analyze the request, modify response as necessary, and + * only call the next item in the filterChain if request is suitably Authenticated. + */ + //TODO Always validate changes against Tomcat AbsCadiValve and Jaspi CadiSAM functions + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest hreq = (HttpServletRequest)request; + HttpServletResponse hresp = (HttpServletResponse)response; + String perm = aafType + hreq.getPathInfo() + '|' + hreq.getMethod(); + if (hreq.isUserInRole(perm)) { + chain.doFilter(request, response); + } else { + log.audit("PathFilter has denied", hreq.getUserPrincipal().getName(), "access to", perm); + hresp.sendError(403, notAuthorizedMsg); + } + } + + /** + * Containers call "destroy" when time to cleanup + */ + public void destroy() { + log.info("PathFilter destroyed."); + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/PermConverter.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/PermConverter.java new file mode 100644 index 00000000..825260cf --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/PermConverter.java @@ -0,0 +1,32 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.filter; + +/** + * Convert a simplistic, single string Permission into an Enterprise Scoped Perm + * + * @author Jonathan + * + */ +public interface PermConverter { + public String convert(String minimal); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/RolesAllowed.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/RolesAllowed.java new file mode 100644 index 00000000..0ffd232a --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/RolesAllowed.java @@ -0,0 +1,56 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +/** + * RolesAllowed + * + * @author Jonathan + * + * Similar to Java EE's Spec from Annotations 1.1, 2.8 + * + * That Spec, however, was geared towards being able to route calls to Methods on Objects, and thus needed a more refined + * sense of permissions hierarchy. The same mechanism, however, can easily be achieved on single Servlet/Handlers in + * POJOs like Jetty by simply adding the Roles Allowed in a similar Annotation + * + */ +package org.onap.ccsdk.apps.cadi.filter; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * JASPI Style Annotation of RolesAllowed when the coding style is desired but actually including all + * JEE jars is not. If using actual JASPI, use official @interface classes, not this one... + * + * @author Jonathan + */ +@Target({TYPE}) +@Retention(RUNTIME) +public @interface RolesAllowed { + /** + * Security role of the implementation, which doesn't have to be an EJB or CORBA like object. Can be just a + * Handler + * @return + */ + String[] value(); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/ServletImpl.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/ServletImpl.java new file mode 100644 index 00000000..404f1e62 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/ServletImpl.java @@ -0,0 +1,56 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +/** + * RolesAllowed + * + * @author Jonathan + * + * Similar to Java EE's Spec from Annotations 1.1, 2.8 + * + * That Spec, however, was geared towards being able to route calls to Methods on Objects, and thus needed a more refined + * sense of permissions hierarchy. The same mechanism, however, can easily be achieved on single Servlet/Handlers in + * POJOs like Jetty by simply adding the Roles Allowed in a similar Annotation + * + */ +package org.onap.ccsdk.apps.cadi.filter; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.servlet.Servlet; + +/** + * + * @author Jonathan + */ +@Target({TYPE}) +@Retention(RUNTIME) +public @interface ServletImpl { + /** + * Security role of the implementation, which doesn't have to be an EJB or CORBA like object. Can be just a + * Handler + * @return + */ + Class<? extends Servlet> value(); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/SideChain.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/SideChain.java new file mode 100644 index 00000000..39b82cc9 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/filter/SideChain.java @@ -0,0 +1,74 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + */ + +package org.onap.ccsdk.apps.cadi.filter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import org.onap.ccsdk.apps.cadi.util.Holder; + +/** + * Add various Filters by CADI Property not in the official Chain + * + * @author Instrumental(Jonathan) + * + */ +public class SideChain { + private List<Filter> sideChain; + + public SideChain() { + sideChain = new ArrayList<Filter>(); + } + + public void add(Filter f) { + sideChain.add(f); + } + + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException { + final Holder<Boolean> hbool = new Holder<Boolean>(Boolean.TRUE); + FilterChain truth = new FilterChain() { + @Override + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + hbool.set(Boolean.TRUE); + } + public String toString() { + return hbool.get().toString(); + } + }; + for(Filter f : sideChain) { + hbool.set(Boolean.FALSE); + f.doFilter(request, response, truth); + if(!hbool.get()) { + return; + } + } + if(hbool.get()) { + chain.doFilter(request, response); + } + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/ConfigPrincipal.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/ConfigPrincipal.java new file mode 100644 index 00000000..3b8f04c1 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/ConfigPrincipal.java @@ -0,0 +1,69 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.lur; + +import java.io.IOException; +import java.security.Principal; + +import org.onap.ccsdk.apps.cadi.GetCred; +import org.onap.ccsdk.apps.cadi.Symm; + +public class ConfigPrincipal implements Principal, GetCred { + private String name; + private byte[] cred; + private String content; + + public ConfigPrincipal(String name, String passwd) { + this.name = name; + this.cred = passwd.getBytes(); + content = null; + } + + public ConfigPrincipal(String name, byte[] cred) { + this.name = name; + this.cred = cred; + content = null; + } + + public String getName() { + return name; + } + + public byte[] getCred() { + return cred; + } + + public String toString() { + return name; + } + + public String getAsBasicAuthHeader() throws IOException { + if (content ==null) { + String s = name + ':' + new String(cred); + content = "Basic " + Symm.base64.encode(s); + } else if (!content.startsWith("Basic ")) { // content is the saved password from construction + String s = name + ':' + content; + content = "Basic " + Symm.base64.encode(s); + } + return content; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/EpiLur.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/EpiLur.java new file mode 100644 index 00000000..3e26fd4d --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/EpiLur.java @@ -0,0 +1,169 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.lur; + +import java.security.Principal; +import java.util.List; + +import org.onap.ccsdk.apps.cadi.CachingLur; +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.CredVal; +import org.onap.ccsdk.apps.cadi.Lur; +import org.onap.ccsdk.apps.cadi.Permission; + +/** + * EpiLUR + * + * Short for "Epic LUR". Be able to run through a series of LURs to obtain the validation needed. + * + * The pun is better for the other pattern... "TAF" (aka EpiTaf), but it's still the larger picture of + * LURs that will be accomplished. + * + * FYI, the reason we separate LURs, rather than combine, is that Various User Repository Resources have + * different Caching requirements. For instance, the Local User Repo (with stand alone names), never expire, but might be + * refreshed with a change in Configuration File, while the Remote Service based LURs will need to expire at prescribed intervals + * + * @author Jonathan + * + */ +public final class EpiLur implements Lur { + private final Lur[] lurs; + + /** + * EpiLur constructor + * + * Construct the EpiLur from variable TAF parameters + * @param lurs + * @throws CadiException + */ + public EpiLur(Lur ... lurs) throws CadiException{ + this.lurs = lurs; + if (lurs.length==0) throw new CadiException("Need at least one Lur implementation in constructor"); + } + + public boolean fish(Principal bait, Permission ... pond) { + if (pond==null) { + return false; + } + boolean rv = false; + Lur lur; + for (int i=0;!rv && i<lurs.length;++i) { + rv = (lur = lurs[i]).fish(bait, pond); + if (!rv && lur.handlesExclusively(pond)) break; + } + return rv; + } + + public void fishAll(Principal bait, List<Permission> permissions) { + for (Lur lur : lurs) { + lur.fishAll(bait, permissions); + } + } + + public void destroy() { + for (Lur lur : lurs) { + lur.destroy(); + } + } + + /** + * Return the first Lur (if any) which also implements UserPass + * @return + */ + public CredVal getUserPassImpl() { + for (Lur lur : lurs) { + if (lur instanceof CredVal) { + return (CredVal)lur; + } + } + return null; + } + + // Never needed... Only EpiLur uses... + public boolean handlesExclusively(Permission ... pond) { + return false; + } + + /** + * Get Lur for index. Returns null if out of range + * @param idx + * @return + */ + public Lur get(int idx) { + if (idx>=0 && idx<lurs.length) { + return lurs[idx]; + } + return null; + } + + public boolean handles(Principal p) { + for (Lur l : lurs) { + if (l.handles(p)) { + return true; + } + } + return false; + } + + public void remove(String id) { + for (Lur l : lurs) { + if (l instanceof CachingLur) { + ((CachingLur<?>)l).remove(id); + } + } + } + + public Lur subLur(Class<? extends Lur> cls ) { + for (Lur l : lurs) { + if (l.getClass().isAssignableFrom(cls)) { + return l; + } + } + return null; + } + + @Override + public Permission createPerm(String p) { + return new LocalPermission(p); + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.Lur#clear(java.security.Principal, java.lang.StringBuilder) + */ + @Override + public void clear(Principal p, StringBuilder report) { + for (Lur lur : lurs) { + lur.clear(p, report); + } + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + for (Lur lur : lurs) { + sb.append(lur.getClass().getSimpleName()); + sb.append(": Report\n"); + sb.append(lur.toString()); + sb.append('\n'); + } + return sb.toString(); + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/LocalLur.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/LocalLur.java new file mode 100644 index 00000000..1a1af6c2 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/LocalLur.java @@ -0,0 +1,221 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.lur; + +import java.io.IOException; +import java.security.Principal; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.onap.ccsdk.apps.cadi.AbsUserCache; +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.CredVal; +import org.onap.ccsdk.apps.cadi.Hash; +import org.onap.ccsdk.apps.cadi.Lur; +import org.onap.ccsdk.apps.cadi.Permission; +import org.onap.ccsdk.apps.cadi.User; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.config.Config; + +/** + * An in-memory Lur that can be configured locally with User info via properties, similar to Tomcat-users.xml mechanisms. + * + * @author Jonathan + * + */ +public final class LocalLur extends AbsUserCache<LocalPermission> implements Lur, CredVal { + public static final String SEMI = "\\s*;\\s*"; + public static final String COLON = "\\s*:\\s*"; + public static final String COMMA = "\\s*,\\s*"; + public static final String PERCENT = "\\s*%\\s*"; + + // Use to quickly determine whether any given group is supported by this LUR + private final Set<String> supportingGroups; + private String supportedRealm; + + /** + * Construct by building structure, see "build" + * + * Reconstruct with "build" + * + * @param userProperties + * @param groupProperties + * @param decryptor + * @throws IOException + */ + public LocalLur(Access access, String userProperties, String groupProperties) throws IOException { + super(access, 0, 0, Integer.MAX_VALUE); // data doesn't expire + supportedRealm = access.getProperty(Config.BASIC_REALM, "localized"); + supportingGroups = new TreeSet<>(); + + if (userProperties != null) { + parseUserProperties(userProperties); + } + + if (groupProperties != null) { + parseGroupProperties(groupProperties); + } + } + + public boolean validate(String user, CredVal.Type type, byte[] cred, Object state) { + if (cred == null) { + return false; + } + User<LocalPermission> usr = getUser(user, cred); + if (usr == null) { + return false; + } + // covers null as well as bad pass + if ((type == Type.PASSWORD) && (usr.principal instanceof ConfigPrincipal)) {; + return Hash.isEqual(cred, ((ConfigPrincipal)usr.principal).getCred()); + } + return false; + } + + // @Override + public boolean fish(Principal bait, Permission ... pond) { + if (pond == null) { + return false; + } + for (Permission p : pond) { + if (handles(bait) && p instanceof LocalPermission) { // local Users only have LocalPermissions + User<LocalPermission> user = getUser(bait); + if (user != null) { + return user.contains((LocalPermission)p); + } + } + } + return false; + } + + // We do not want to expose the actual Group, so make a copy. + public void fishAll(Principal bait, List<Permission> perms) { + if (handles(bait)) { + User<LocalPermission> user = getUser(bait); + if (user != null) { + user.copyPermsTo(perms); + } + } + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.Lur#handles(java.security.Principal) + */ + @Override + public boolean handles(Principal principal) { + if (principal == null) { + return false; + } + return principal.getName().endsWith(supportedRealm); + } + + @Override + public boolean handlesExclusively(Permission ... pond) { + boolean rv = false; + for (Permission p : pond) { + if (rv=supportingGroups.contains(p.getKey())) { + break; + } + } + return rv; + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.Lur#createPerm(java.lang.String) + */ + @Override + public Permission createPerm(String p) { + return new LocalPermission(p); + } + + private void parseUserProperties(String userProperties) throws IOException { + // For each User name... + for (String userProperty : userProperties.trim().split(SEMI)) { + String[] userInfo = userProperty.split(COLON, 2); + String[] userPass = userInfo[0].split(PERCENT, 2); + String userName = userPass[0]; + + byte[] password = null; + if (userPass.length > 1) { + password = access.decrypt(userPass[1], true).getBytes(); + if (userName.indexOf('@') < 0) { + userName += '@' + access.getProperty(Config.AAF_DEFAULT_REALM, Config.getDefaultRealm()); + } + } + User<LocalPermission> usr; + usr = new User<>(new ConfigPrincipal(userName, password)); + addUser(usr); + access.log(Level.INIT, "Local User:", usr.principal); + + if (userInfo.length > 1) { + Map<String, Permission> newMap = usr.newMap(); + for (String group : userInfo[1].split(COMMA)) { + supportingGroups.add(group); + usr.add(newMap, new LocalPermission(group)); + } + usr.setMap(newMap); + } + } + } + + + private void parseGroupProperties(String groupProperties) throws IOException { + // For each Group name... + for (String group : groupProperties.trim().split(SEMI)) { + String[] groups = group.split(COLON, 2); + if (groups.length <= 1) { + continue; + } + supportingGroups.add(groups[0]); + LocalPermission p = new LocalPermission(groups[0]); + + // Add all users (known by comma separators) + for (String groupMember : groups[1].split(COMMA)) { + // look for password, if so, put in passMap + String[] userPass = groupMember.split(PERCENT, 2); + String userName = userPass[0]; + if (userName.indexOf('@') < 0) { + userName += '@' + access.getProperty(Config.AAF_DEFAULT_REALM, Config.getDefaultRealm()); + } + + User<LocalPermission> usr = null; + byte[] password = null; + if (userPass.length > 1) { + password = access.decrypt(userPass[1], true).getBytes(); + } + usr = getUser(userName, password); + if (usr == null) { + usr = new User<>(new ConfigPrincipal(userName, password)); + addUser(usr); + } + else { + usr.principal = new ConfigPrincipal(userName, password); + } + usr.add(p); + access.log(Level.INIT, "Local User:", usr.principal); + } + } + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/LocalPermission.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/LocalPermission.java new file mode 100644 index 00000000..a6aa1124 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/LocalPermission.java @@ -0,0 +1,50 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.lur; + +import org.onap.ccsdk.apps.cadi.Permission; + +public class LocalPermission implements Permission { + private String key; + + public LocalPermission(String role) { + this.key = role; + } + + public String getKey() { + return key; + } + + public String toString() { + return key; + } + + public boolean match(Permission p) { + return key.equals(p.getKey()); + } + + public String permType() { + return "LOCAL"; + } + + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/NullLur.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/NullLur.java new file mode 100644 index 00000000..32c09204 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/lur/NullLur.java @@ -0,0 +1,87 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.lur; + +import java.security.Principal; +import java.util.List; + +import org.onap.ccsdk.apps.cadi.Lur; +import org.onap.ccsdk.apps.cadi.Permission; + +public class NullLur implements Lur { + private static final Permission NULL = new Permission() { + @Override + public String permType() { + return ""; + } + + @Override + public String getKey() { + return ""; + } + + @Override + public boolean match(Permission p) { + return false; + }}; + + public boolean fish(Principal bait, Permission ... pond) { + // Well, for Jenkins, this is ok... It finds out it can't do J2EE Security, and then looks at it's own +// System.err.println("CADI's LUR has not been configured, but is still being called. Access is being denied"); + return false; + } + + public void fishAll(Principal bait, List<Permission> permissions) { + } + + public void destroy() { + } + + public boolean handlesExclusively(Permission ... pond) { + return false; + } + + public boolean handles(Principal p) { + return false; + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.Lur#createPerm(java.lang.String) + */ + @Override + public Permission createPerm(String p) { + return NULL; + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.Lur#clear(java.security.Principal, java.lang.StringBuilder) + */ + @Override + public void clear(Principal p, StringBuilder report) { + report.append(NullLur.class.getSimpleName()); + report.append('\n'); + } + + public String toString() { + return NullLur.class.getSimpleName() + '\n'; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/BasicPrincipal.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/BasicPrincipal.java new file mode 100644 index 00000000..ef0499cc --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/BasicPrincipal.java @@ -0,0 +1,133 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.principal; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Date; + +import org.onap.ccsdk.apps.cadi.BasicCred; +import org.onap.ccsdk.apps.cadi.GetCred; +import org.onap.ccsdk.apps.cadi.Symm; + +public class BasicPrincipal extends BearerPrincipal implements GetCred { + private static byte[] basic = "Basic ".getBytes(); + + private String name = null; + private String shortName = null; + private String domain; + private byte[] cred = null; + private long created; + + + public BasicPrincipal(String content,String defaultDomain) throws IOException { + created = System.currentTimeMillis(); + ByteArrayInputStream bis = new ByteArrayInputStream(content.getBytes()); + // Read past "Basic ", ensuring it starts with it. + for (int i=0;i<basic.length;++i) { + if (bis.read()!=basic[i]) { + name=content; + cred = null; + return; + } + } + BasicOS bos = new BasicOS(content.length()); + Symm.base64.decode(bis,bos); // note: writes directly to name until ':' + if (name==null) throw new IOException("Invalid Coding"); + else cred = bos.toCred(); + int at; + if ((at=name.indexOf('@'))>0) { + domain=name.substring(at+1); + shortName=name.substring(0, at); + } else { + shortName = name; + domain=defaultDomain; + name = name + '@' + defaultDomain; + } + } + + public BasicPrincipal(BasicCred bc, String domain) { + name = bc.getUser(); + cred = bc.getCred(); + this.domain = domain; + } + + private class BasicOS extends OutputStream { + private boolean first = true; + private ByteArrayOutputStream baos; + + public BasicOS(int size) { + baos = new ByteArrayOutputStream(size); + } + + @Override + public void write(int b) throws IOException { + if (b==':' && first) { + first = false; + name = new String(baos.toByteArray()); + baos.reset(); // + } else { + baos.write(b); + } + } + + private byte[] toCred() { + return baos.toByteArray(); + } + } + + public String getName() { + return name; + } + + public String getShortName() { + return shortName; + } + + public String getDomain() { + return domain; + } + + public byte[] getCred() { + return cred; + } + + public long created() { + return created; + } + + public String toString() { + return "Basic Authorization for " + name + " evaluated on " + new Date(created).toString(); + } + + @Override + public String tag() { + return "BAth"; + } + + @Override + public String personalName() { + return name; // personalName not available with Basic Auth + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/BearerPrincipal.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/BearerPrincipal.java new file mode 100644 index 00000000..4d416594 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/BearerPrincipal.java @@ -0,0 +1,33 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.principal; + +public abstract class BearerPrincipal extends TaggedPrincipal { + private String bearer = null; + public BearerPrincipal setBearer(String bearer) { + this.bearer = bearer; + return this; + } + public String getBearer() { + return bearer; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/CachedBasicPrincipal.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/CachedBasicPrincipal.java new file mode 100644 index 00000000..7df46964 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/CachedBasicPrincipal.java @@ -0,0 +1,65 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.principal; + +import java.io.IOException; + +import org.onap.ccsdk.apps.cadi.BasicCred; +import org.onap.ccsdk.apps.cadi.CachedPrincipal; +import org.onap.ccsdk.apps.cadi.taf.HttpTaf; + +/** + * Cached Principals need to be able to revalidate in the Background + * + * @author Jonathan + * + */ +public class CachedBasicPrincipal extends BasicPrincipal implements CachedPrincipal { + private final HttpTaf creator; + private long timeToLive; + private long expires; + + public CachedBasicPrincipal(HttpTaf creator, BasicCred bc, String domain, long timeToLive) { + super(bc, domain); + this.creator = creator; + this.timeToLive = timeToLive; + expires = System.currentTimeMillis()+timeToLive; + } + + public CachedBasicPrincipal(HttpTaf creator, String content, String domain, long timeToLive) throws IOException { + super(content, domain); + this.creator = creator; + this.timeToLive = timeToLive; + expires = System.currentTimeMillis()+timeToLive; + } + + public CachedPrincipal.Resp revalidate(Object state) { + Resp resp = creator.revalidate(this, state); + if (resp.equals(Resp.REVALIDATED))expires = System.currentTimeMillis()+timeToLive; + return resp; + } + + public long expires() { + return expires; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/Kind.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/Kind.java new file mode 100644 index 00000000..702ac5fe --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/Kind.java @@ -0,0 +1,53 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.principal; + +import java.security.Principal; + +public class Kind { + public static final char X509 = 'X'; + public static final char OAUTH = 'O'; + public static final char AAF_OAUTH='A'; + public static final char BASIC_AUTH = 'B'; + public static final char UNKNOWN = 'U'; + + + public static char getKind(final Principal principal) { + Principal check; + if (principal instanceof TrustPrincipal) { + check = ((TrustPrincipal)principal).original(); + } else { + check = principal; + } + if (check instanceof X509Principal) { + return X509; + } + if (check instanceof OAuth2FormPrincipal) { + // Note: if AAF, will turn into 'A' + return OAUTH; + } + if (check instanceof BasicPrincipal) { + return BASIC_AUTH; + } + return UNKNOWN; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/OAuth2FormPrincipal.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/OAuth2FormPrincipal.java new file mode 100644 index 00000000..a1836337 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/OAuth2FormPrincipal.java @@ -0,0 +1,61 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.principal; + +public class OAuth2FormPrincipal extends TaggedPrincipal { + private final String username; + private final String client_id; + + /* + * Note: client_id and username might be the same, if only authenticating the Client_ID + */ + public OAuth2FormPrincipal(final String client_id, final String username) { + this.username = username; + this.client_id = client_id; + } + + @Override + public String getName() { + return username; + } + + public String client_id() { + return client_id; + } + + @Override + public String tag() { + return "OAuth"; + } + + @Override + public String personalName() { + if (username!=null && username!=client_id) { + StringBuilder sb = new StringBuilder(); + sb.append(username); + sb.append('|'); + sb.append(client_id); + return sb.toString(); + } + return client_id; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/StringTagLookup.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/StringTagLookup.java new file mode 100644 index 00000000..d6bf8616 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/StringTagLookup.java @@ -0,0 +1,35 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ +package org.onap.ccsdk.apps.cadi.principal; + +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.principal.TaggedPrincipal.TagLookup; + +public class StringTagLookup implements TagLookup { + private String tag; + public StringTagLookup(final String tag) { + this.tag = tag; + } + @Override + public String lookup() throws CadiException { + return tag; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/TaggedPrincipal.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/TaggedPrincipal.java new file mode 100644 index 00000000..72b6e6db --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/TaggedPrincipal.java @@ -0,0 +1,60 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ +package org.onap.ccsdk.apps.cadi.principal; + +import java.security.Principal; + +import org.onap.ccsdk.apps.cadi.CadiException; + +public abstract class TaggedPrincipal implements Principal { + + public TaggedPrincipal() { + tagLookup = null; + } + + public TaggedPrincipal(final TagLookup tl) { + tagLookup = tl; + } + + public abstract String tag(); // String representing what kind of Authentication occurred. + + public interface TagLookup { + public String lookup() throws CadiException; + } + + private TagLookup tagLookup; + + public void setTagLookup(TagLookup tl) { + tagLookup = tl; + } + + public String personalName() { + if (tagLookup == null) { + return getName(); + } + try { + return tagLookup.lookup(); + } catch (CadiException e) { + return getName(); + } + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/TrustPrincipal.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/TrustPrincipal.java new file mode 100644 index 00000000..1d875a2e --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/TrustPrincipal.java @@ -0,0 +1,70 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.principal; + +import java.security.Principal; + +import org.onap.ccsdk.apps.cadi.UserChain; + +public class TrustPrincipal extends BearerPrincipal implements UserChain { + private final String name; + private final Principal original; + private String userChain; + + public TrustPrincipal(final Principal actual, final String asName) { + this.original = actual; + name = asName.trim(); + if (actual instanceof UserChain) { + UserChain uc = (UserChain)actual; + userChain = uc.userChain(); + } else if (actual instanceof TaggedPrincipal) { + userChain=((TaggedPrincipal)actual).tag(); + } else { + userChain = actual.getClass().getSimpleName(); + } + } + + @Override + public String getName() { + return name; + } + + @Override + public String userChain() { + return userChain; + } + + public Principal original() { + return original; + } + + @Override + public String tag() { + return userChain; + } + + @Override + public String personalName() { + return original.getName() + '[' + userChain + ']'; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/UnAuthPrincipal.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/UnAuthPrincipal.java new file mode 100644 index 00000000..5f6e0e41 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/UnAuthPrincipal.java @@ -0,0 +1,37 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.principal; + +import java.security.Principal; + +public class UnAuthPrincipal implements Principal { + private String name; + + public UnAuthPrincipal(final String name) { + this.name = name; + } + @Override + public String getName() { + return name; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/X509Principal.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/X509Principal.java new file mode 100644 index 00000000..245b27a9 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/principal/X509Principal.java @@ -0,0 +1,112 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.principal; + +import java.io.IOException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.regex.Pattern; + +import org.onap.ccsdk.apps.cadi.GetCred; +import org.onap.ccsdk.apps.cadi.taf.basic.BasicHttpTaf; + +public class X509Principal extends BearerPrincipal implements GetCred { + private static final Pattern pattern = Pattern.compile("[a-zA-Z0-9]*\\@[a-zA-Z0-9.]*"); + private final X509Certificate cert; + private final String name; + private byte[] content; + private BasicHttpTaf bht; + + public X509Principal(String identity, X509Certificate cert) { + name = identity; + content = null; + this.cert = cert; + } + + public X509Principal(String identity, X509Certificate cert, byte[] content, BasicHttpTaf bht) { + name = identity; + this.content = content; + this.cert = cert; + this.bht = bht; + } + + public X509Principal(X509Certificate cert, byte[] content, BasicHttpTaf bht) throws IOException { + this.content=content; + this.cert = cert; + String _name = null; + String subj = cert.getSubjectDN().getName(); + int cn = subj.indexOf("OU="); + if (cn>=0) { + cn+=3; + int space = subj.indexOf(',',cn); + if (space>=0) { + String id = subj.substring(cn, space); + if (pattern.matcher(id).matches()) { + _name = id; + } + } + } + if (_name==null) { + throw new IOException("X509 does not have Identity as CN"); + } + name = _name; + this.bht = bht; + } + + public String getAsHeader() throws IOException { + try { + if (content==null) { + content=cert.getEncoded(); + } + } catch (CertificateEncodingException e) { + throw new IOException(e); + } + return "X509 " + content; + } + + public String toString() { + return "X509 Authentication for " + name; + } + + + public byte[] getCred() { + try { + return content==null?(content=cert.getEncoded()):content; + } catch (CertificateEncodingException e) { + return null; + } + } + + public String getName() { + return name; + } + + @Override + public String tag() { + return "x509"; + } + + public BasicHttpTaf getBasicHttpTaf() { + return bht; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/AbsTafResp.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/AbsTafResp.java new file mode 100644 index 00000000..478d7ea7 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/AbsTafResp.java @@ -0,0 +1,168 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.principal.TaggedPrincipal; +import org.onap.ccsdk.apps.cadi.util.Timing; + +/** + * AbsTafResp + * + * Base class for TafResp (TAF Response Objects) + * + * @author Jonathan + * + */ +public abstract class AbsTafResp implements TafResp { + + protected final Access access; + protected final String tafName; + // Note: Valid Resp is based on Principal being non-null + protected final TaggedPrincipal principal; + protected final String target; + protected final String desc; + private float timing; + + /** + * AbsTafResp + * + * Set and hold + * Description (for logging) + * Principal (as created by derived class) + * Access (for access to underlying container, i.e. for Logging, auditing, ClassLoaders, etc) + * + * @param access + * @param tafname + * @param principal + * @param description + */ + public AbsTafResp(Access access, String tafname, TaggedPrincipal principal, String description) { + this.access = access; + this.tafName = tafname; + this.principal = principal; + this.target = principal==null?"unknown":principal.getName(); + this.desc = description; + } + + /** + * AbsTafResp + * + * Set and hold + * Description (for logging) + * Principal (as created by derived class) + * Access (for access to underlying container, i.e. for Logging, auditing, ClassLoaders, etc) + * + * @param access + * @param tafname + * @param principal + * @param description + */ + public AbsTafResp(Access access, String tafname, String target, String description) { + this.access = access; + this.tafName = tafname; + this.principal = null; + this.target = target; + this.desc = description; + } + + /** + * isValid() + * + * Respond in the affirmative if the TAF was able to Authenticate + */ + public boolean isValid() { + return principal != null; + } + + /** + * desc() + * + * Respond with description of response as given by the TAF + */ + public String desc() { + return desc; + } + + /** + * isAuthenticated() + * + * Respond with the TAF's code of whether Authenticated, or suggested next steps + * default is either IS_AUTHENTICATED, or TRY_ANOTHER_TAF. The TAF can overload + * and suggest others, such as "NO_FURTHER_PROCESSING", if it can detect that this + * is some sort of security breach (i.e. Denial of Service) + */ + public RESP isAuthenticated() { + return principal==null?RESP.TRY_ANOTHER_TAF:RESP.IS_AUTHENTICATED; + } + + /** + * getPrincipal() + * + * Return the principal created by the TAF based on Authentication. + * + * Returns "null" if Authentication failed (no principal) + */ + public TaggedPrincipal getPrincipal() { + return principal; + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.taf.TafResp#getTarget() + */ + @Override + public String getTarget() { + return target; + } + + /** + * getAccess() + * + * Get the Access object from the TAF, so that appropriate Logging, etc can be coordinated. + */ + public Access getAccess() { + return access; + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.taf.TafResp#isFailedAttempt() + */ + public boolean isFailedAttempt() { + return false; + } + + @Override + public float timing() { + return timing; + } + + @Override + public void timing(final long start) { + timing = Timing.millis(start); + } + + @Override + public String taf() { + return tafName; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/EpiTaf.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/EpiTaf.java new file mode 100644 index 00000000..b4102b0d --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/EpiTaf.java @@ -0,0 +1,84 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf; + +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.Taf; + +/** + * EpiTAF + * + * Short for "Epic TAF". Be able to run through a series of TAFs to obtain the validation needed. + * + * OK, the name could probably be better as "Tafs", like it was originally, but the pun was too + * irresistible for this author to pass up. + * + * @author Jonathan + * + */ +public class EpiTaf implements Taf { + private Taf[] tafs; + + /** + * EpiTaf constructor + * + * Construct the EpiTaf from variable TAF parameters + * @param tafs + * @throws CadiException + */ + public EpiTaf(Taf ... tafs) throws CadiException{ + this.tafs = tafs; + if (tafs.length==0) throw new CadiException("Need at least one Taf implementation in constructor"); + } + + /** + * validate + * + * Respond with the first TAF to authenticate user based on variable info and "LifeForm" (is it + * a human behind an interface, or a server behind a protocol). + * + * If there is no TAF that can authenticate, respond with the first TAF that suggests it can + * establish an Authentication conversation (TRY_AUTHENTICATING). + * + * If no TAF declares either, respond with NullTafResp (which denies all questions) + */ + public TafResp validate(LifeForm reading, String... info) { + TafResp tresp,firstTryAuth=null; + for (Taf taf : tafs) { + tresp = taf.validate(reading, info); + switch(tresp.isAuthenticated()) { + case TRY_ANOTHER_TAF: + break; + case TRY_AUTHENTICATING: + if (firstTryAuth==null)firstTryAuth=tresp; + break; + default: + return tresp; + } + } + + // No TAFs configured, at this point. It is safer at this point to be "not validated", + // rather than "let it go" + return firstTryAuth == null?NullTafResp.singleton():firstTryAuth; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/HttpEpiTaf.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/HttpEpiTaf.java new file mode 100644 index 00000000..ca792abe --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/HttpEpiTaf.java @@ -0,0 +1,214 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf; + +import java.net.URI; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.CachedPrincipal; +import org.onap.ccsdk.apps.cadi.CachedPrincipal.Resp; +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.Locator; +import org.onap.ccsdk.apps.cadi.Taf.LifeForm; +import org.onap.ccsdk.apps.cadi.TrustChecker; + +/** + * HttpEpiTaf + * + * An extension of the basic "EpiTAF" concept, check known HTTP Related TAFs for valid credentials + * + * @author Jonathan + * + */ +public class HttpEpiTaf implements HttpTaf { + private HttpTaf[] tafs; + private Access access; + private Locator<URI> locator; + private TrustChecker trustChecker; + + /** + * HttpEpiTaf constructor + * + * Construct the HttpEpiTaf from variable Http specific TAF parameters + + * @param tafs + * @throws CadiException + */ + public HttpEpiTaf(Access access, Locator<URI> locator, TrustChecker tc, HttpTaf ... tafs) throws CadiException{ + this.tafs = tafs; + this.access = access; + this.locator = locator; + this.trustChecker = tc; + // Establish what Header Property to look for UserChain/Trust Props + + if (tafs.length == 0) { + throw new CadiException("Need at least one HttpTaf implementation in constructor"); + } + } + + /** + * validate + * + * Respond with the first Http specific TAF to authenticate user based on variable info + * and "LifeForm" (is it a human behind a browser, or a server utilizing HTTP Protocol). + * + * If there is no HttpTAF that can authenticate, respond with the first TAF that suggests it can + * establish an Authentication conversation (TRY_AUTHENTICATING) (Examples include a redirect to CSP + * Servers for CSP Cookie, or BasicAuth 401 response, suggesting User/Password for given Realm + * submission + * + * If no TAF declares either, respond with NullTafResp (which denies all questions) + */ + public TafResp validate(LifeForm reading, HttpServletRequest req, HttpServletResponse resp) { + // Given a LifeForm Neutral, for HTTP, we need to discover true Life-Form Readings + if (reading == LifeForm.LFN) { + reading = tricorderScan(req); + } + TafResp tresp = null; + TafResp firstTry = null; + List<Redirectable> redirectables = null; + List<TafResp> log; + if (access.willLog(Level.DEBUG)) { + log = new ArrayList<>(); + } else { + log = null; + } + try { + for (HttpTaf taf : tafs) { + final long start = System.nanoTime(); + tresp = taf.validate(reading, req, resp); + addToLog(log, tresp, start); + switch(tresp.isAuthenticated()) { + case TRY_ANOTHER_TAF: + break; // and loop + case TRY_AUTHENTICATING: + if (tresp instanceof Redirectable) { + if (redirectables == null) { + redirectables = new ArrayList<>(); + } + redirectables.add((Redirectable)tresp); + } else if (firstTry == null) { + firstTry = tresp; + } + break; + case IS_AUTHENTICATED: + tresp = trustChecker.mayTrust(tresp, req); + return tresp; + default: + return tresp; + } + } + } finally { + printLog(log); + } + + // If No TAFs configured, at this point. It is safer at this point to be "not validated", + // rather than "let it go" + // Note: if exists, there will always be more than 0 entries, according to above code + if (redirectables == null) { + return (firstTry != null) ? firstTry : NullTafResp.singleton(); + } + + // If there is one Tryable entry then return it + if (redirectables.size() > 1) { + return LoginPageTafResp.create(access, locator, resp, redirectables); + } else { + return redirectables.get(0); + } + } + + public boolean revalidate(Principal prin) throws Exception { + return false; + } + + /* + * Since this is internal, we use a little Star Trek humor to indicate looking in the HTTP Request to see if we can determine what kind + * of "LifeForm" reading we can determine, i.e. is there a Human (CarbonBasedLifeForm) behind a browser, or is it mechanical + * id (SiliconBasedLifeForm)? This makes a difference in some Authentication, i.e CSP, which doesn't work well for SBLFs + */ + private LifeForm tricorderScan(HttpServletRequest req) { + // For simplicity's sake, we'll say Humans use FQDNs, not IPs. + + // Current guess that only Browsers bother to set "Agent" codes that identify the kind of browser they are. + // If mechanical frameworks are found that populate this, then more advanced analysis may be required + // Jonathan 1/22/2013 + String agent = req.getHeader("User-Agent"); + if (agent != null && agent.startsWith("Mozilla")) { // covers I.E./Firefox/Safari/probably any other "advanced" Browser see http://en.wikipedia.org/wiki/User_agent + return LifeForm.CBLF; + } + return LifeForm.SBLF; // notably skips "curl","wget", (which is desired behavior. We don't want to try CSP, etc on these) + } + + public Resp revalidate(CachedPrincipal prin, Object state) { + Resp resp; + for (HttpTaf taf : tafs) { + resp = taf.revalidate(prin, state); + if (resp != Resp.NOT_MINE) { + return resp; + } +// switch(resp) { +// case NOT_MINE: +// break; +// default: +// return resp; +// } + } + return Resp.NOT_MINE; + } + + private void addToLog(List<TafResp> log, final TafResp tresp, final long start) { + if (log == null) { + return; + } + tresp.timing(start); + log.add(tresp); + } + + private void printLog(List<TafResp> log) { + if (log == null) { + return; + } + for (TafResp tresp : log) { + access.printf(Level.DEBUG, "%s: %s, ms=%f", tresp.taf(), tresp.desc(), tresp.timing()); + } + } + + /** + * List HttpTafs with their "toString" representations... primarily useful for Debugging in an IDE + * like Eclipse. + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + for (HttpTaf ht : tafs) { + sb.append(ht.toString()); + sb.append(". "); + } + return sb.toString(); + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/HttpTaf.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/HttpTaf.java new file mode 100644 index 00000000..d2ac5900 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/HttpTaf.java @@ -0,0 +1,60 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.onap.ccsdk.apps.cadi.CachedPrincipal; +import org.onap.ccsdk.apps.cadi.Taf.LifeForm; + +/** + * A TAF which is in a specific HTTP environment in which the engine implements + * javax Servlet. + * + * Using the Http Request and Response interfaces takes the effort out of implementing in almost any kind of + * HTTP Container or Engine. + * + * @author Jonathan + * + */ +public interface HttpTaf { + /** + * validate + * + * Validate the Request, and respond with created TafResp object. + * + * @param reading + * @param req + * @param resp + * @return + */ + public TafResp validate(LifeForm reading, HttpServletRequest req, HttpServletResponse resp); + + /** + * Re-Validate Credential + * + * @param prin + * @return + */ + public CachedPrincipal.Resp revalidate(CachedPrincipal prin,Object state); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/LoginPageTafResp.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/LoginPageTafResp.java new file mode 100644 index 00000000..43aac666 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/LoginPageTafResp.java @@ -0,0 +1,100 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf; + +import java.io.IOException; +import java.net.URI; +import java.util.List; + +import jakarta.servlet.http.HttpServletResponse; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.Locator; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.Locator.Item; + +public class LoginPageTafResp extends AbsTafResp { + private final HttpServletResponse httpResp; + private final String loginPageURL; + + private LoginPageTafResp(Access access, final HttpServletResponse resp, String loginPageURL) { + super(access, "LoginPage","unknown", "Multiple Possible HTTP Logins available. Redirecting to Login Choice Page"); + httpResp = resp; + this.loginPageURL = loginPageURL; + } + + @Override + public RESP authenticate() throws IOException { + httpResp.sendRedirect(loginPageURL); + return RESP.HTTP_REDIRECT_INVOKED; + } + + @Override + public RESP isAuthenticated() { + return RESP.TRY_AUTHENTICATING; + } + + public static TafResp create(Access access, Locator<URI> locator, final HttpServletResponse resp, List<Redirectable> redirectables) { + if (locator == null) { + if (!redirectables.isEmpty()) { + access.log(Level.DEBUG,"LoginPage Locator is not configured. Taking first Redirectable Taf"); + return redirectables.get(0); + } + return NullTafResp.singleton(); + } + + try { + Item item = locator.best(); + URI uri = locator.get(item); + if (uri == null) { + return NullTafResp.singleton(); + } + + StringBuilder sb = new StringBuilder(uri.toString()); + String query = uri.getQuery(); + boolean first = ((query == null) || (query.length() == 0)); + for (Redirectable redir : redirectables) { + if (first) { + sb.append('?'); + first = false; + } + else { + sb.append('&'); + } + sb.append(redir.get()); + } + if (!redirectables.isEmpty()) { + return new LoginPageTafResp(access, resp, sb.toString()); + } + } catch (Exception e) { + access.log(e, "Error deriving Login Page location"); + } + + return NullTafResp.singleton(); + } + + @Override + public String taf() { + return "LoginPage"; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/NullTaf.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/NullTaf.java new file mode 100644 index 00000000..9fb1697a --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/NullTaf.java @@ -0,0 +1,64 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.onap.ccsdk.apps.cadi.CachedPrincipal; +import org.onap.ccsdk.apps.cadi.Taf; +import org.onap.ccsdk.apps.cadi.CachedPrincipal.Resp; + + +/** + * This TAF is set at the very beginning of Filters and Valves so that if any configuration issues hit while + * starting, the default behavior is to shut down traffic rather than leaving an open hole + * + * @author Jonathan + * + */ +public class NullTaf implements Taf, HttpTaf { + // Singleton Pattern + public NullTaf() {} + + /** + * validate + * + * Always Respond with a NullTafResp, which declares it is unauthenticated, and unauthorized + */ + public TafResp validate(LifeForm reading, String... info) { + return NullTafResp.singleton(); + } + + /** + * validate + * + * Always Respond with a NullTafResp, which declares it is unauthenticated, and unauthorized + */ + public TafResp validate(LifeForm reading, HttpServletRequest req, HttpServletResponse resp) { + return NullTafResp.singleton(); + } + + public Resp revalidate(CachedPrincipal prin, Object state) { + return Resp.NOT_MINE; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/NullTafResp.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/NullTafResp.java new file mode 100644 index 00000000..f93baee9 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/NullTafResp.java @@ -0,0 +1,96 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf; + +import java.io.IOException; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.principal.TaggedPrincipal; + +/** + * A Null Pattern for setting responses to "Deny" before configuration is setup. + * @author Jonathan + * + */ +class NullTafResp implements TafResp { + private NullTafResp(){} + + private static TafResp singleton = new NullTafResp(); + + public static TafResp singleton() { + return singleton; + } + + public boolean isValid() { + return false; + } + + public RESP isAuthenticated() { + return RESP.NO_FURTHER_PROCESSING; + } + + public String desc() { + return "All Authentication denied"; + } + + public RESP authenticate() throws IOException { + return RESP.NO_FURTHER_PROCESSING; + } + + public TaggedPrincipal getPrincipal() { + return null; + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.taf.TafResp#getTarget() + */ + @Override + public String getTarget() { + return "unknown"; + } + + public Access getAccess() { + return Access.NULL; + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.taf.TafResp#isFailedAttempt() + */ + public boolean isFailedAttempt() { + return true; + } + + @Override + public float timing() { + return 0; + } + + @Override + public void timing(long start) { + } + + @Override + public String taf() { + return "NULL"; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/PuntTafResp.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/PuntTafResp.java new file mode 100644 index 00000000..3b028c09 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/PuntTafResp.java @@ -0,0 +1,97 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf; + +import java.io.IOException; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.principal.TaggedPrincipal; +import org.onap.ccsdk.apps.cadi.util.Timing; + +/** + * A Punt Resp to make it fast and easy for a Taf to respond that it cannot handle a particular kind of + * request. It is always the same object, so there is no cost for memory, etc. + * @author Jonathan + * + */ +public class PuntTafResp implements TafResp { + private final String name; + private final String desc; + private float timing; + + public PuntTafResp(String name, String explanation) { + this.name = name; + desc = "Not processing this transaction: " + explanation; + } + + public boolean isValid() { + return false; + } + + public RESP isAuthenticated() { + return RESP.TRY_ANOTHER_TAF; + } + + public String desc() { + return desc; + } + + public RESP authenticate() throws IOException { + return RESP.TRY_ANOTHER_TAF; + } + + public TaggedPrincipal getPrincipal() { + return null; + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.taf.TafResp#getTarget() + */ + @Override + public String getTarget() { + return "punt"; + } + + public Access getAccess() { + return NullTafResp.singleton().getAccess(); + } + + public boolean isFailedAttempt() { + return false; + } + + @Override + public float timing() { + return timing; + } + + @Override + public void timing(long start) { + timing = Timing.millis(start); + } + + @Override + public String taf() { + return name; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/Redirectable.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/Redirectable.java new file mode 100644 index 00000000..358d6b2f --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/Redirectable.java @@ -0,0 +1,31 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf; + +public interface Redirectable extends TafResp { + /** + * Create a Redirectable URL entry prefaced by a URLEncoder.String for a Menu + * example: + * "Global Login=https://xxxx....." + */ + public String get(); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/TafResp.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/TafResp.java new file mode 100644 index 00000000..80923815 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/TafResp.java @@ -0,0 +1,116 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf; + +import java.io.IOException; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.principal.TaggedPrincipal; + +/** + * Response from Taf objects, which inform users what has happened and/or what should be done + * + * @author Jonathan + * + */ +public interface TafResp { + public static enum RESP { + IS_AUTHENTICATED, + NO_FURTHER_PROCESSING, + TRY_AUTHENTICATING, + TRY_ANOTHER_TAF, + FAIL, + // A note was made to avoid the response REDIRECT. However, I have deemed that it is + // unavoidable when the underlying TAF did do a REDIRECT, because it requires a HTTP + // Service code to exit without modifying the Response any further. + // Therefore, I have changed this to indicate what HAS happened, with should accommodate + // both positions. Jonathan 10/18/2012 +// public static final int HTTP_REDIRECT_INVOKED = 11; + HTTP_REDIRECT_INVOKED, + HAS_PROCESSED}; + + /** + * Basic success check + * @return + */ + public boolean isValid(); + + /** + * String description of what has occurred (for logging/exceptions) + * @return + */ + public String desc(); + + /** + * Check Response + * @return + */ + public RESP isAuthenticated(); + + /** + * Authenticate, returning FAIL or Other Valid indication + * + * HTTP implementations should watch for "HTTP_REDIRECT_INVOKED", and end the HTTP call appropriately. + * @return + * @throws CadiException + */ + public RESP authenticate() throws IOException; + + /** + * Once authenticated, this object should hold a Principal created from the authorization + * @return + */ + public TaggedPrincipal getPrincipal(); + + /** Target - when Authentication Fails, need to know what ID was being attempted + * @return + */ + public String getTarget(); + + /** + * get the Access object which created this object, allowing the responder to appropriate Log, etc + */ + public Access getAccess(); + + /** + * Be able to check if part of a Failed attempt + */ + public boolean isFailedAttempt(); + + /** + * report how long this took + * @return + */ + public float timing(); + + /** + * Set end of timing in Millis, given Nanos + * @param start + */ + void timing(long start); + + /** + * Support Taf Name + */ + String taf(); +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/TrustNotTafResp.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/TrustNotTafResp.java new file mode 100644 index 00000000..4f2b0404 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/TrustNotTafResp.java @@ -0,0 +1,102 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf; + +import java.io.IOException; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.principal.TaggedPrincipal; +import org.onap.ccsdk.apps.cadi.util.Timing; + +public class TrustNotTafResp implements TafResp { + private final TafResp delegate; + private final String desc; + private float timing; + + public TrustNotTafResp(final TafResp delegate, final String desc) { + this.delegate = delegate; + this.desc = desc; + } + + @Override + public boolean isValid() { + return false; + } + + @Override + public String desc() { + return desc; + } + + @Override + public RESP isAuthenticated() { + return RESP.NO_FURTHER_PROCESSING; + } + + @Override + public RESP authenticate() throws IOException { + return RESP.NO_FURTHER_PROCESSING; + } + + @Override + public TaggedPrincipal getPrincipal() { + return delegate.getPrincipal(); + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.taf.TafResp#getTarget() + */ + @Override + public String getTarget() { + return delegate.getTarget(); + } + + @Override + public Access getAccess() { + return delegate.getAccess(); + } + + @Override + public boolean isFailedAttempt() { + return true; + } + @Override + public float timing() { + return timing; + } + + @Override + public void timing(long start) { + timing = Timing.millis(start); + } + + @Override + public String toString() { + return desc(); + } + + @Override + public String taf() { + return "TrustNot"; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/TrustTafResp.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/TrustTafResp.java new file mode 100644 index 00000000..e3a4e80b --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/TrustTafResp.java @@ -0,0 +1,103 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf; + +import java.io.IOException; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.principal.TaggedPrincipal; +import org.onap.ccsdk.apps.cadi.util.Timing; + +public class TrustTafResp implements TafResp { + private final TafResp delegate; + private final TaggedPrincipal principal; + private final String desc; + private float timing; + + public TrustTafResp(final TafResp delegate, final TaggedPrincipal principal, final String desc) { + this.delegate = delegate; + this.principal = principal; + this.desc = desc + ' ' + delegate.desc(); + } + + @Override + public boolean isValid() { + return delegate.isValid(); + } + + @Override + public String desc() { + return desc; + } + + @Override + public RESP isAuthenticated() { + return delegate.isAuthenticated(); + } + + @Override + public RESP authenticate() throws IOException { + return delegate.authenticate(); + } + + @Override + public TaggedPrincipal getPrincipal() { + return principal; + } + + /* (non-Javadoc) + * @see org.onap.ccsdk.apps.cadi.taf.TafResp#getTarget() + */ + @Override + public String getTarget() { + return delegate.getTarget(); + } + + @Override + public Access getAccess() { + return delegate.getAccess(); + } + + @Override + public boolean isFailedAttempt() { + return delegate.isFailedAttempt(); + } + @Override + public float timing() { + return timing; + } + + @Override + public void timing(long start) { + timing = Timing.millis(start); + } + + public String toString() { + return principal.getName() + " by trust of " + desc(); + } + + @Override + public String taf() { + return "Trust"; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/basic/BasicHttpTaf.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/basic/BasicHttpTaf.java new file mode 100644 index 00000000..54b98d82 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/basic/BasicHttpTaf.java @@ -0,0 +1,219 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf.basic; + +import java.io.IOException; +import java.security.Principal; +import java.util.Map; +import java.util.TreeMap; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.BasicCred; +import org.onap.ccsdk.apps.cadi.CachedPrincipal; +import org.onap.ccsdk.apps.cadi.CachedPrincipal.Resp; +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.CredVal; +import org.onap.ccsdk.apps.cadi.CredVal.Type; +import org.onap.ccsdk.apps.cadi.CredValDomain; +import org.onap.ccsdk.apps.cadi.Taf; +import org.onap.ccsdk.apps.cadi.config.Config; +import org.onap.ccsdk.apps.cadi.filter.MapBathConverter; +import org.onap.ccsdk.apps.cadi.principal.BasicPrincipal; +import org.onap.ccsdk.apps.cadi.principal.CachedBasicPrincipal; +import org.onap.ccsdk.apps.cadi.taf.HttpTaf; +import org.onap.ccsdk.apps.cadi.taf.TafResp; +import org.onap.ccsdk.apps.cadi.taf.TafResp.RESP; +import org.onap.ccsdk.apps.cadi.taf.dos.DenialOfServiceTaf; +import org.onap.ccsdk.apps.cadi.util.CSV; + +/** + * BasicHttpTaf + * + * This TAF implements the "Basic Auth" protocol. + * + * WARNING! It is true for any implementation of "Basic Auth" that the password is passed unencrypted. + * This is because the expectation, when designed years ago, was that it would only be used in + * conjunction with SSL (https). It is common, however, for users to ignore this on the assumption that + * their internal network is secure, or just ignorance. Therefore, a WARNING will be printed + * when the HTTP Channel is not encrypted (unless explicitly turned off). + * + * @author Jonathan + * + */ +public class BasicHttpTaf implements HttpTaf { + private Access access; + private String realm; + private CredVal rbac; + private Map<String,CredVal> rbacs = new TreeMap<>(); + private boolean warn; + private long timeToLive; + private MapBathConverter mapIds; + + public BasicHttpTaf(Access access, CredVal rbac, String realm, long timeToLive, boolean turnOnWarning) { + this.access = access; + this.realm = realm; + this.rbac = rbac; + this.warn = turnOnWarning; + this.timeToLive = timeToLive; + String csvFile = access.getProperty(Config.CADI_BATH_CONVERT, null); + if(csvFile==null) { + mapIds=null; + } else { + try { + mapIds = new MapBathConverter(access, new CSV(access,csvFile)); + } catch (IOException | CadiException e) { + access.log(e,"Bath Map Conversion is not initialzed (non fatal)"); + } + } + } + + public void add(final CredValDomain cvd) { + rbacs.put(cvd.domain(), cvd); + } + + /** + * Note: BasicHttp works for either Carbon Based (Humans) or Silicon Based (machine) Lifeforms. + * @see Taf + */ + public TafResp validate(Taf.LifeForm reading, HttpServletRequest req, HttpServletResponse resp) { + // See if Request implements BasicCred (aka CadiWrap or other), and if User/Pass has already been set separately + if (req instanceof BasicCred) { + BasicCred bc = (BasicCred)req; + if (bc.getUser()!=null) { // CadiWrap, if set, makes sure User & Password are both valid, or both null + if (DenialOfServiceTaf.isDeniedID(bc.getUser())!=null) { + return DenialOfServiceTaf.respDenyID(access,bc.getUser()); + } + CachedBasicPrincipal bp = new CachedBasicPrincipal(this,bc,realm,timeToLive); + + // Be able to do Organizational specific lookups by Domain + CredVal cv = rbacs.get(bp.getDomain()); + if (cv==null) { + cv = rbac; + } + + // ONLY FOR Last Ditch DEBUGGING... + // access.log(Level.WARN,bp.getName() + ":" + new String(bp.getCred())); + if (cv.validate(bp.getName(),Type.PASSWORD,bp.getCred(),req)) { + return new BasicHttpTafResp(access,bp,bp.getName()+" authenticated by password",RESP.IS_AUTHENTICATED,resp,realm,false); + } else { + //TODO may need timed retries in a given time period + return new BasicHttpTafResp(access,bc.getUser(),buildMsg(bp,req,"user/pass combo invalid for ",bc.getUser(),"from",req.getRemoteAddr()), + RESP.TRY_AUTHENTICATING,resp,realm,true); + } + } + } + // Get User/Password from Authorization Header value + String authz = req.getHeader("Authorization"); + String target="unknown"; + + if (authz != null && authz.startsWith("Basic ")) { + if (warn&&!req.isSecure()) { + access.log(Level.WARN,"WARNING! BasicAuth has been used over an insecure channel"); + } + if(mapIds != null) { + authz = mapIds.convert(access, authz); + } + try { + CachedBasicPrincipal ba = new CachedBasicPrincipal(this,authz,realm,timeToLive); + target=ba.getName(); + if (DenialOfServiceTaf.isDeniedID(ba.getName())!=null) { + return DenialOfServiceTaf.respDenyID(access,ba.getName()); + } + + final int at = ba.getName().indexOf('@'); + CredVal cv = rbacs.get(ba.getName().substring(at+1)); + if (cv==null) { + cv = rbac; // default + } + + // ONLY FOR Last Ditch DEBUGGING... + // access.log(Level.WARN,ba.getName() + ":" + new String(ba.getCred())); + if (cv.validate(ba.getName(), Type.PASSWORD, ba.getCred(), req)) { + return new BasicHttpTafResp(access,ba, ba.getName()+" authenticated by BasicAuth password",RESP.IS_AUTHENTICATED,resp,realm,false); + } else { + //TODO may need timed retries in a given time period + return new BasicHttpTafResp(access,target,buildMsg(ba,req,"user/pass combo invalid"), + RESP.TRY_AUTHENTICATING,resp,realm,true); + } + } catch (IOException e) { + String msg = buildMsg(null,req,"Failed HTTP Basic Authorization (", e.getMessage(), ')'); + access.log(Level.INFO,msg); + return new BasicHttpTafResp(access,target,msg, RESP.TRY_AUTHENTICATING, resp, realm,true); + } + } + return new BasicHttpTafResp(access,target,"Requesting HTTP Basic Authorization",RESP.TRY_AUTHENTICATING,resp,realm,false); + } + + protected String buildMsg(Principal pr, HttpServletRequest req, Object ... msg) { + StringBuilder sb = new StringBuilder(); + if (pr!=null) { + sb.append("user="); + sb.append(pr.getName()); + sb.append(','); + } + sb.append("ip="); + sb.append(req.getRemoteAddr()); + sb.append(",port="); + sb.append(req.getRemotePort()); + if (msg.length>0) { + sb.append(",msg=\""); + for (Object s : msg) { + sb.append(s.toString()); + } + sb.append('"'); + } + return sb.toString(); + } + + public void addCredVal(final String realm, final CredVal cv) { + rbacs.put(realm, cv); + } + + public CredVal getCredVal(String key) { + CredVal cv = rbacs.get(key); + if (cv==null) { + cv = rbac; + } + return cv; + } + + @Override + public Resp revalidate(CachedPrincipal prin, Object state) { + if (prin instanceof BasicPrincipal) { + BasicPrincipal ba = (BasicPrincipal)prin; + if (DenialOfServiceTaf.isDeniedID(ba.getName())!=null) { + return Resp.UNVALIDATED; + } + return rbac.validate(ba.getName(), Type.PASSWORD, ba.getCred(), state)?Resp.REVALIDATED:Resp.UNVALIDATED; + } + return Resp.NOT_MINE; + } + + public String toString() { + return "Basic Auth enabled on realm: " + realm; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/basic/BasicHttpTafResp.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/basic/BasicHttpTafResp.java new file mode 100644 index 00000000..9fc8fc67 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/basic/BasicHttpTafResp.java @@ -0,0 +1,69 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf.basic; + +import java.io.IOException; + +import jakarta.servlet.http.HttpServletResponse; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.principal.TaggedPrincipal; +import org.onap.ccsdk.apps.cadi.taf.AbsTafResp; +import org.onap.ccsdk.apps.cadi.taf.TafResp; + +public class BasicHttpTafResp extends AbsTafResp implements TafResp { + private static final String tafName = BasicHttpTaf.class.getSimpleName(); + private HttpServletResponse httpResp; + private String realm; + private RESP status; + private final boolean wasFailed; + + public BasicHttpTafResp(Access access, TaggedPrincipal principal, String description, RESP status, HttpServletResponse resp, String realm, boolean wasFailed) { + super(access, tafName, principal, description); + httpResp = resp; + this.realm = realm; + this.status = status; + this.wasFailed = wasFailed; + } + + public BasicHttpTafResp(Access access, String target, String description, RESP status, HttpServletResponse resp, String realm, boolean wasFailed) { + super(access, tafName, target, description); + httpResp = resp; + this.realm = realm; + this.status = status; + this.wasFailed = wasFailed; + } + + public RESP authenticate() throws IOException { + httpResp.setStatus(401); // Unauthorized + httpResp.setHeader("WWW-Authenticate", "Basic realm=\""+realm+'"'); + return RESP.HTTP_REDIRECT_INVOKED; + } + + public RESP isAuthenticated() { + return status; + } + + public boolean isFailedAttempt() { + return wasFailed; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/cert/CertIdentity.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/cert/CertIdentity.java new file mode 100644 index 00000000..d60ee8cd --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/cert/CertIdentity.java @@ -0,0 +1,46 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf.cert; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import jakarta.servlet.http.HttpServletRequest; + +import org.onap.ccsdk.apps.cadi.principal.TaggedPrincipal; + +public interface CertIdentity { + /** + * identity from X509Certificate Object and/or certBytes + * + * If you have both, include them. If you only have one, leave the other null, and it will be generated if needed + * + * The Request is there to obtain Header or Attribute info of ultimate user + * + * @param req + * @param cert + * @param certBytes + * @return + * @throws CertificateException + */ + public TaggedPrincipal identity(HttpServletRequest req, X509Certificate cert, byte[] certBytes) throws CertificateException; +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/cert/X509HttpTafResp.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/cert/X509HttpTafResp.java new file mode 100644 index 00000000..70fb5788 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/cert/X509HttpTafResp.java @@ -0,0 +1,52 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf.cert; + +import java.io.IOException; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.principal.TaggedPrincipal; +import org.onap.ccsdk.apps.cadi.taf.AbsTafResp; + +public class X509HttpTafResp extends AbsTafResp { + private static final String tafName = X509Taf.class.getSimpleName(); + + private RESP status; + + public X509HttpTafResp(Access access, TaggedPrincipal principal, String description, RESP status) { + super(access, tafName, principal, description); + this.status = status; + } + + public RESP authenticate() throws IOException { + return RESP.TRY_ANOTHER_TAF; + } + + public RESP isAuthenticated() { + return status; + } + + public String toString() { + return status.name(); + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/cert/X509Taf.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/cert/X509Taf.java new file mode 100644 index 00000000..c854523e --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/cert/X509Taf.java @@ -0,0 +1,300 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf.cert; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; + +import javax.net.ssl.TrustManagerFactory; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.CachedPrincipal; +import org.onap.ccsdk.apps.cadi.CachedPrincipal.Resp; +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.CredVal; +import org.onap.ccsdk.apps.cadi.Lur; +import org.onap.ccsdk.apps.cadi.Symm; +import org.onap.ccsdk.apps.cadi.Taf.LifeForm; +import org.onap.ccsdk.apps.cadi.config.Config; +import org.onap.ccsdk.apps.cadi.config.SecurityInfo; +import org.onap.ccsdk.apps.cadi.config.SecurityInfoC; +import org.onap.ccsdk.apps.cadi.principal.TaggedPrincipal; +import org.onap.ccsdk.apps.cadi.principal.X509Principal; +import org.onap.ccsdk.apps.cadi.taf.HttpTaf; +import org.onap.ccsdk.apps.cadi.taf.TafResp; +import org.onap.ccsdk.apps.cadi.taf.TafResp.RESP; +import org.onap.ccsdk.apps.cadi.taf.basic.BasicHttpTaf; +import org.onap.ccsdk.apps.cadi.util.Split; + +public class X509Taf implements HttpTaf { + private static final String CERTIFICATE_NOT_VALID_FOR_AUTHENTICATION = "Certificate NOT valid for Authentication"; + public static final CertificateFactory certFactory; + public static final MessageDigest messageDigest; + public static final TrustManagerFactory tmf; + private Access access; + private CertIdentity[] certIdents; +// private Lur lur; + private ArrayList<String> cadiIssuers; + private String env; + private SecurityInfo si; + private BasicHttpTaf bht; + + static { + try { + certFactory = CertificateFactory.getInstance("X.509"); + messageDigest = MessageDigest.getInstance("SHA-256"); // use this to clone + tmf = TrustManagerFactory.getInstance(SecurityInfoC.SSL_KEY_MANAGER_FACTORY_ALGORITHM); + } catch (Exception e) { + throw new RuntimeException("X.509 and SHA-256 are required for X509Taf",e); + } + } + + public X509Taf(Access access, Lur lur, CertIdentity ... cis) throws CertificateException, NoSuchAlgorithmException, CadiException { + this.access = access; + env = access.getProperty(Config.AAF_ENV,null); + if (env==null) { + throw new CadiException("X509Taf requires Environment ("+Config.AAF_ENV+") to be set."); + } +// this.lur = lur; + this.cadiIssuers = new ArrayList<>(); + for (String ci : access.getProperty(Config.CADI_X509_ISSUERS, "").split(":")) { + access.printf(Level.INIT, "Trusting Identity for Certificates signed by \"%s\"",ci); + cadiIssuers.add(ci); + } + try { + Class<?> dci = access.classLoader().loadClass("org.onap.ccsdk.apps.auth.direct.DirectCertIdentity"); + if (dci==null) { + certIdents = cis; + } else { + CertIdentity temp[] = new CertIdentity[cis.length+1]; + System.arraycopy(cis, 0, temp, 1, cis.length); + temp[0] = (CertIdentity) dci.newInstance(); + certIdents=temp; + } + } catch (Exception e) { + certIdents = cis; + } + + si = new SecurityInfo(access); + } + + public static final X509Certificate getCert(byte[] certBytes) throws CertificateException { + ByteArrayInputStream bais = new ByteArrayInputStream(certBytes); + return (X509Certificate)certFactory.generateCertificate(bais); + } + + public static final byte[] getFingerPrint(byte[] ba) { + MessageDigest md; + try { + md = (MessageDigest)messageDigest.clone(); + } catch (CloneNotSupportedException e) { + // should never get here + return new byte[0]; + } + md.update(ba); + return md.digest(); + } + + @Override + public TafResp validate(LifeForm reading, HttpServletRequest req, HttpServletResponse resp) { + // Check for Mutual SSL + try { + X509Certificate[] certarr = (X509Certificate[])req.getAttribute("javax.servlet.request.X509Certificate"); + if (certarr!=null && certarr.length>0) { + si.checkClientTrusted(certarr); + // Note: If the Issuer is not in the TrustStore, it's not added to the Cert list + String issuer = certarr[0].getIssuerDN().toString(); + String subject = certarr[0].getSubjectDN().getName(); + access.printf(Level.DEBUG,"Client Certificate found\n Subject '%s'\n Issuer '%s'",subject,issuer); + if (cadiIssuers.contains(issuer)) { + // avoiding extra object creation, since this is validated EVERY transaction with a Cert + int start = 0; + int end = 1; + int comma; + int length = subject.length(); + + compare: + while(start<length) { + while(Character.isWhitespace(subject.charAt(start))) { + if(++start>length) { + break compare; + } + } + comma = subject.indexOf(',',start); + if(comma<0) { + end = subject.length(); + } else { + end = comma<=0?0:comma-1; + } + while(Character.isWhitespace(subject.charAt(end))) { + if(--end < 0) { + break compare; + } + } + if(subject.regionMatches(start, "OU=", 0, 3) || + subject.regionMatches(start, "CN=", 0, 3)) { + int at = subject.indexOf('@', start); + if(at<end && at>=0) { + String[] sa = Split.splitTrim(':', subject, start+3,end+1); + if (sa.length==1 || (sa.length>1 && env!=null && env.equals(sa[1]))) { // Check Environment + return new X509HttpTafResp(access, + new X509Principal(sa[0], certarr[0],(byte[])null,bht), + "X509Taf validated " + sa[0] + (sa.length<2?"":" for aaf_env " + env ), RESP.IS_AUTHENTICATED); + } else { + access.printf(Level.DEBUG,"Certificate is not for environment '%s'",env); + break; + } + } + } + start = comma+1; + } + access.log(Level.DEBUG,"Certificate is not acceptable for Authentication"); + } else { + access.log(Level.DEBUG,"Issuer is not trusted for Authentication"); + } + } else { + access.log(Level.DEBUG,"There is no client certificate on the transaction"); + } + + + byte[] array = null; + byte[] certBytes = null; + X509Certificate cert=null; + String responseText=null; + String authHeader = req.getHeader("Authorization"); + + if (certarr!=null) { // If cert !=null, Cert is Tested by Mutual Protocol. + if (authHeader!=null) { // This is only intended to be a Secure Connection, not an Identity + for (String auth : Split.split(',',authHeader)) { + if (auth.startsWith("Bearer ")) { // Bearer = OAuth... Don't use as Authenication + return new X509HttpTafResp(access, null, "Certificate verified, but Bearer Token is presented", RESP.TRY_ANOTHER_TAF); + } + } + } + cert = certarr[0]; + responseText = ", validated by Mutual SSL Protocol"; + } else { // If cert == null, Get Declared Cert (in header), but validate by having them sign something + if (authHeader != null) { + for (String auth : Split.splitTrim(',',authHeader)) { + if (auth.startsWith("x509 ")) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(auth.length()); + try { + array = auth.getBytes(); + ByteArrayInputStream bais = new ByteArrayInputStream(array); + Symm.base64noSplit.decode(bais, baos, 5); + certBytes = baos.toByteArray(); + cert = getCert(certBytes); + + /** + * Identity from CERT if well know CA and specific encoded information + */ + // If found Identity doesn't work, try SignedStuff Protocol + // cert.checkValidity(); + // cert.--- GET FINGERPRINT? + String stuff = req.getHeader("Signature"); + if (stuff==null) + return new X509HttpTafResp(access, null, "Header entry 'Signature' required to validate One way X509 Certificate", RESP.TRY_ANOTHER_TAF); + String data = req.getHeader("Data"); + // if (data==null) + // return new X509HttpTafResp(access, null, "No signed Data to validate with X509 Certificate", RESP.TRY_ANOTHER_TAF); + + // Note: Data Pos shows is "<signatureType> <data>" + // int dataPos = (stuff.indexOf(' ')); // determine what is Algorithm + // Get Signature + bais = new ByteArrayInputStream(stuff.getBytes()); + baos = new ByteArrayOutputStream(stuff.length()); + Symm.base64noSplit.decode(bais, baos); + array = baos.toByteArray(); + // Signature sig = Signature.getInstance(stuff.substring(0, dataPos)); // get Algorithm from first part of Signature + + Signature sig = Signature.getInstance(cert.getSigAlgName()); + sig.initVerify(cert.getPublicKey()); + sig.update(data.getBytes()); + if (!sig.verify(array)) { + access.log(Level.ERROR, "Signature doesn't Match"); + return new X509HttpTafResp(access, null, CERTIFICATE_NOT_VALID_FOR_AUTHENTICATION, RESP.TRY_ANOTHER_TAF); + } + responseText = ", validated by Signed Data"; + } catch (Exception e) { + access.log(e, "Exception while validating Cert"); + return new X509HttpTafResp(access, null, CERTIFICATE_NOT_VALID_FOR_AUTHENTICATION, RESP.TRY_ANOTHER_TAF); + } + } + } + } + if (cert==null) { + return new X509HttpTafResp(access, null, "No Certificate Info on Transaction", RESP.TRY_ANOTHER_TAF); + } + + // A cert has been found, match Identify + TaggedPrincipal prin=null; + + for (int i=0;prin==null && i<certIdents.length;++i) { + if ((prin=certIdents[i].identity(req, cert, certBytes))!=null) { + responseText = prin.getName() + " matches Certificate " + cert.getSubjectX500Principal().getName() + responseText; + } + } + + // if Principal is found, check for "AS_USER" and whether this entity is trusted to declare + if (prin!=null) { + // Note: Tag for Certs is Fingerprint, but that takes computation... leaving off + return new X509HttpTafResp( + access, + prin, + responseText, + RESP.IS_AUTHENTICATED); + } + } + } catch (Exception e) { + return new X509HttpTafResp(access, null, e.getMessage(), RESP.TRY_ANOTHER_TAF); + } + + return new X509HttpTafResp(access, null, "Certificate cannot be used for authentication", RESP.TRY_ANOTHER_TAF); + } + + @Override + public Resp revalidate(CachedPrincipal prin, Object state) { + return null; + } + + public void add(BasicHttpTaf bht) { + this.bht = bht; + } + + public CredVal getCredVal(final String key) { + if (bht==null) { + return null; + } else { + return bht.getCredVal(key); + } + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/dos/DenialOfServiceTaf.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/dos/DenialOfServiceTaf.java new file mode 100644 index 00000000..483334fa --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/dos/DenialOfServiceTaf.java @@ -0,0 +1,375 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf.dos; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.CachedPrincipal; +import org.onap.ccsdk.apps.cadi.CadiException; +import org.onap.ccsdk.apps.cadi.CachedPrincipal.Resp; +import org.onap.ccsdk.apps.cadi.Taf.LifeForm; +import org.onap.ccsdk.apps.cadi.config.Config; +import org.onap.ccsdk.apps.cadi.taf.HttpTaf; +import org.onap.ccsdk.apps.cadi.taf.PuntTafResp; +import org.onap.ccsdk.apps.cadi.taf.TafResp; +import org.onap.ccsdk.apps.cadi.taf.TafResp.RESP; + +public class DenialOfServiceTaf implements HttpTaf { + private static Map<String, Counter> deniedIP=null, deniedID=null; + private Access access; + private final TafResp puntNotDenied; + private static File dosIP, dosID; + + /** + * + * @param hostname + * @param prod + * @throws CadiException + */ + public DenialOfServiceTaf(Access access) throws CadiException { + puntNotDenied = new PuntTafResp("DenialOfServiceTaf", "This Transaction is not denied"); + this.access = access; + if (dosIP==null || dosID == null) { + String dirStr; + if ((dirStr = access.getProperty(Config.AAF_DATA_DIR, null))!=null) { + dosIP = new File(dirStr+"/dosIP"); + readIP(); + dosID = new File(dirStr+"/dosID"); + readID(); + } + } + } + + @Override + public TafResp validate(LifeForm reading, HttpServletRequest req, final HttpServletResponse resp) { + // Performance, when not needed + if (deniedIP != null) { + String ip; + Counter c = deniedIP.get(ip=req.getRemoteAddr()); + if (c!=null) { + c.inc(); + return respDenyIP(access,ip); + } + } + + // Note: Can't process Principal, because this is the first TAF, and no Principal is created. + // Other TAFs use "isDenied()" on this Object to validate. + return puntNotDenied; + } + + @Override + public Resp revalidate(CachedPrincipal prin, Object state) { + // We always return NOT MINE, because DOS Taf does not ever validate + return Resp.NOT_MINE; + } + + /* + * for use in Other TAFs, before they attempt backend validation of + */ + public static Counter isDeniedID(String identity) { + if (deniedID!=null) { + return deniedID.get(identity); + } + return null; + } + + /** + * + */ + public static Counter isDeniedIP(String ipvX) { + if (deniedIP!=null) { + return deniedIP.get(ipvX); + } + return null; + } + + /** + * Return of "True" means IP has been added. + * Return of "False" means IP already added. + * + * @param ip + * @return + */ + public static synchronized boolean denyIP(String ip) { + boolean rv = false; + if (deniedIP==null) { + deniedIP = new HashMap<>(); + deniedIP.put(ip, new Counter(ip)); // Noted duplicated for minimum time spent + rv= true; + } else if (deniedIP.get(ip)==null) { + deniedIP.put(ip, new Counter(ip)); + rv = true; + } + if (rv) { + writeIP(); + } + return rv; + } + + private static void writeIP() { + if (dosIP!=null && deniedIP!=null) { + if (deniedIP.isEmpty()) { + if (dosIP.exists()) { + dosIP.delete(); + } + } else { + PrintStream fos; + try { + fos = new PrintStream(new FileOutputStream(dosIP,false)); + try { + for (String ip: deniedIP.keySet()) { + fos.println(ip); + } + } finally { + fos.close(); + } + } catch (IOException e) { + e.printStackTrace(System.err); + } + } + } + } + + private static void readIP() { + if (dosIP!=null && dosIP.exists()) { + BufferedReader br; + try { + br = new BufferedReader(new FileReader(dosIP)); + try { + if (deniedIP==null) { + deniedIP=new HashMap<>(); + } + + String line; + while ((line=br.readLine())!=null) { + deniedIP.put(line, new Counter(line)); + } + } finally { + br.close(); + } + } catch (IOException e) { + e.printStackTrace(System.err); + } + } + } + + + /** + * Return of "True" means IP has was removed. + * Return of "False" means IP wasn't being denied. + * + * @param ip + * @return + */ + public static synchronized boolean removeDenyIP(String ip) { + if (deniedIP!=null && deniedIP.remove(ip)!=null) { + writeIP(); + if (deniedIP.isEmpty()) { + deniedIP=null; + } + return true; + } + return false; + } + + /** + * Return of "True" means ID has been added. + * Return of "False" means ID already added. + * + * @param ip + * @return + */ + public static synchronized boolean denyID(String id) { + boolean rv = false; + if (deniedID==null) { + deniedID = new HashMap<>(); + deniedID.put(id, new Counter(id)); // Noted duplicated for minimum time spent + rv = true; + } else if (deniedID.get(id)==null) { + deniedID.put(id, new Counter(id)); + rv = true; + } + if (rv) { + writeID(); + } + return rv; + + } + + private static void writeID() { + if (dosID!=null && deniedID!=null) { + if (deniedID.isEmpty()) { + if (dosID.exists()) { + dosID.delete(); + } + } else { + PrintStream fos; + try { + fos = new PrintStream(new FileOutputStream(dosID,false)); + try { + for (String ip: deniedID.keySet()) { + fos.println(ip); + } + } finally { + fos.close(); + } + } catch (IOException e) { + e.printStackTrace(System.err); + } + } + } + } + + private static void readID() { + if (dosID!=null && dosID.exists()) { + BufferedReader br; + try { + br = new BufferedReader(new FileReader(dosID)); + try { + if (deniedID==null) { + deniedID=new HashMap<>(); + } + + String line; + while ((line=br.readLine())!=null) { + deniedID.put(line, new Counter(line)); + } + } finally { + br.close(); + } + } catch (IOException e) { + e.printStackTrace(System.err); + } + } + } + + /** + * Return of "True" means ID has was removed. + * Return of "False" means ID wasn't being denied. + * + * @param ip + * @return + */ + public static synchronized boolean removeDenyID(String id) { + if (deniedID!=null && deniedID.remove(id)!=null) { + writeID(); + if (deniedID.isEmpty()) { + deniedID=null; + } + + return true; + } + return false; + } + + public List<String> report() { + int initSize = 0; + if (deniedIP!=null)initSize+=deniedIP.size(); + if (deniedID!=null)initSize+=deniedID.size(); + ArrayList<String> al = new ArrayList<>(initSize); + if (deniedID!=null) { + for (Counter c : deniedID.values()) { + al.add(c.toString()); + } + } + if (deniedIP!=null) { + for (Counter c : deniedIP.values()) { + al.add(c.toString()); + } + } + return al; + } + + public static class Counter { + private final String name; + private int count = 0; + private Date first; + private long last; // note, we use "last" as long, to avoid popping useless dates on Heap. + + public Counter(String name) { + this.name = name; + first = null; + last = 0L; + count = 0; + } + + public String getName() { + return name; + } + + public int getCount() { + return count; + } + + public long getLast() { + return last; + } + + /* + * Only allow Denial of ServiceTaf to increment + */ + private synchronized void inc() { + ++count; + last = System.currentTimeMillis(); + if (first==null) { + first = new Date(last); + } + } + + public String toString() { + if (count==0) + return name + " is on the denied list, but has not attempted Access"; + else + return + name + + " has been denied " + + count + + " times since " + + first + + ". Last denial was " + + new Date(last); + } + } + + public static TafResp respDenyID(Access access, String identity) { + return new DenialOfServiceTafResp(access, RESP.NO_FURTHER_PROCESSING, identity + " is on the Identity Denial list"); + } + + public static TafResp respDenyIP(Access access, String ip) { + return new DenialOfServiceTafResp(access, RESP.NO_FURTHER_PROCESSING, ip + " is on the IP Denial list"); + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/dos/DenialOfServiceTafResp.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/dos/DenialOfServiceTafResp.java new file mode 100644 index 00000000..1ebb2e66 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/taf/dos/DenialOfServiceTafResp.java @@ -0,0 +1,55 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.taf.dos; + +import java.io.IOException; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.taf.AbsTafResp; + +public class DenialOfServiceTafResp extends AbsTafResp { + private static final String tafName = DenialOfServiceTaf.class.getSimpleName(); + + private RESP ect; // Homage to Arethra Franklin + + public DenialOfServiceTafResp(Access access, RESP resp, String description ) { + super(access, tafName, "dos", description); + ect = resp; + } + + // Override base behavior of checking Principal and trying another TAF + @Override + public RESP isAuthenticated() { + return ect; + } + + + public RESP authenticate() throws IOException { + return ect; + } + + @Override + public String taf() { + return "DOS"; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/CSV.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/CSV.java new file mode 100644 index 00000000..89eb70c0 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/CSV.java @@ -0,0 +1,290 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + */ + +package org.onap.ccsdk.apps.cadi.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +import org.onap.ccsdk.apps.cadi.Access; +import org.onap.ccsdk.apps.cadi.Access.Level; +import org.onap.ccsdk.apps.cadi.CadiException; + +/** + * Read CSV file for various purposes + * + * @author Instrumental(Jonathan) + * + */ +public class CSV { + private File csv; + private Access access; + private boolean processAll; + private char delimiter = ','; + private boolean go; + + public CSV(Access access, File file) { + this.access = access; + csv = file; + processAll = false; + go = true; + } + + public CSV(Access access, String csvFilename) { + this.access = access; + csv = new File(csvFilename); + processAll = false; + go = true; + } + + public CSV setDelimiter(char delimiter) { + this.delimiter = delimiter; + return this; + } + + public String name() { + return csv.getName(); + } + + public CSV processAll() { + processAll = true; + return this; + } + /* + * Create your code to accept the List<String> row. + * + * Your code may keep the List... CSV does not hold onto it. + * + * @author Instrumental(Jonathan) + * + */ + public interface Visitor { + void visit(List<String> row) throws IOException, CadiException; + } + + public void visit(Visitor visitor) throws IOException, CadiException { + BufferedReader br = new BufferedReader(new FileReader(csv)); + try { + String line; + StringBuilder sb = new StringBuilder(); + while(go && (line = br.readLine())!=null) { + line=line.trim(); + if(!line.startsWith("#") && line.length()>0) { +// System.out.println(line); uncomment to debug + List<String> row = new ArrayList<>(); + boolean quotes=false; + boolean escape=false; + char c = 0; + for(int i=0;i<line.length();++i) { + switch(c=line.charAt(i)) { + case '"': + if(quotes) { + if(i<line.length()-1) { // may look ahead + if('"' == line.charAt(i+1)) { + sb.append(c); + ++i; + } else { + quotes = false; + } + } else { + quotes=false; + } + } else { + quotes=true; + } + break; + case '\\': + if(escape) { + sb.append(c); + escape = false; + } else { + escape = true; + } + break; + case 'n': + if(escape) { + sb.append("\\n"); + escape=false; + } else { + sb.append(c); + } + break; + default: + if(delimiter==c) { + if(quotes) { + sb.append(c); + } else { + row.add(sb.toString()); + sb.setLength(0); + } + } else { + sb.append(c); + } + } + } + if(sb.length()>0 || c==',') { + row.add(sb.toString()); + sb.setLength(0); + } + try { + visitor.visit(row); + } catch (CadiException e) { + if(processAll) { + access.log(Level.ERROR,e); + } else { + throw e; + } + } + } + } + } finally { + br.close(); + } + } + + public Writer writer() throws FileNotFoundException { + return new Writer(false); + } + + public Writer writer(boolean append) throws FileNotFoundException { + return new Writer(append); + } + + public interface RowSetter { + public void row(Object ... objs); + } + + public static class Saver implements RowSetter { + List<String> ls= new ArrayList<>(); + + @Override + public void row(Object ... objs) { + if(objs.length>0) { + for(Object o : objs) { + if(o != null) { + if(o instanceof String[]) { + for(String str : (String[])o) { + ls.add(str); + } + } else { + ls.add(o.toString()); + } + } + } + } + } + + public List<String> asList() { + List<String> rv = ls; + ls = new ArrayList<>(); + return rv; + } + } + + public class Writer implements RowSetter { + private PrintStream ps; + private Writer(final boolean append) throws FileNotFoundException { + ps = new PrintStream(new FileOutputStream(csv,append)); + } + + @Override + public void row(Object ... objs) { + if(objs.length>0) { + boolean first = true; + for(Object o : objs) { + if(first) { + first = false; + } else { + ps.append(delimiter); + } + if(o == null) { + } else if(o instanceof String[]) { + for(String str : (String[])o) { + print(str); + } + } else { + print(o.toString()); + } + } + ps.println(); + } + } + + private void print(String s) { + boolean quote = s.matches(".*[,|\"].*"); + if(quote) { + ps.append('"'); + ps.print(s.replace("\"", "\"\"") + .replace("'", "''") + .replace("\\", "\\\\")); + ps.append('"'); + } else { + ps.append(s); + } + + + } + /** + * Note: CSV files do not actually support Comments as a standard, but it is useful + * @param comment + */ + public void comment(String comment, Object ... objs) { + ps.print("# "); + ps.printf(comment,objs); + ps.println(); + } + + public void flush() { + ps.flush(); + } + + public void close() { + flush(); + ps.close(); + } + + public String toString() { + return csv.getAbsolutePath(); + } + } + + /** + * Provides a way to stop processing records from inside a Visit + */ + public void stop() { + go = false; + } + + public void delete() { + csv.delete(); + } + + public String toString() { + return csv.getAbsolutePath(); + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Chmod.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Chmod.java new file mode 100644 index 00000000..de029bb4 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Chmod.java @@ -0,0 +1,62 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.util; + +import java.io.File; +import java.io.IOException; + +public interface Chmod { + public void chmod(File f) throws IOException; + + public static final Chmod to755 = new Chmod() { + public void chmod(File f) throws IOException { + f.setExecutable(true, false); + f.setExecutable(true, true); + f.setReadable(true, false); + f.setReadable(true, true); + f.setWritable(false, false); + f.setWritable(true, true); + } + }; + + public static final Chmod to644 = new Chmod() { + public void chmod(File f) throws IOException { + f.setExecutable(false, false); + f.setExecutable(false, true); + f.setReadable(true, false); + f.setReadable(true, true); + f.setWritable(false, false); + f.setWritable(true, true); + } + }; + + public static final Chmod to400 = new Chmod() { + public void chmod(File f) throws IOException { + f.setExecutable(false, false); + f.setExecutable(false, true); + f.setReadable(false, false); + f.setReadable(true, true); + f.setWritable(false, false); + f.setWritable(false, true); + } + }; +}
\ No newline at end of file diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/FQI.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/FQI.java new file mode 100644 index 00000000..66b59b17 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/FQI.java @@ -0,0 +1,51 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.util; + +public class FQI { + /** + * Take a Fully Qualified User, and get a Namespace from it. + * @param fqi + * @return + */ + public final static String reverseDomain(final String fqi) { + StringBuilder sb = null; + String[] split = Split.split('.',fqi); + int at; + for (int i=split.length-1;i>=0;--i) { + if (sb == null) { + sb = new StringBuilder(); + } else { + sb.append('.'); + } + + if ((at = split[i].indexOf('@'))>0) { + sb.append(split[i].subSequence(at+1, split[i].length())); + } else { + sb.append(split[i]); + } + } + + return sb==null?"":sb.toString(); + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/FixURIinfo.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/FixURIinfo.java new file mode 100644 index 00000000..2dbc7c9c --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/FixURIinfo.java @@ -0,0 +1,75 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + */ + +package org.onap.ccsdk.apps.cadi.util; + +import java.net.URI; + +/** + * URI and URL, if the host does not have "dots", will interpret Host:port as Authority + * + * This is very problematic for Containers, which like single name entries. + * @author Instrumental(Jonathan) + * + */ +public class FixURIinfo { + private String auth; + private String host; + private int port; + + public FixURIinfo(URI uri) { + auth = uri.getAuthority(); + host = uri.getHost(); + if(host==null || (auth!=null && auth.startsWith(host))) { + if(auth!=null) { + int colon = auth.indexOf(':'); + if(colon >= 0 ) { + host = auth.substring(0, colon); + port = Integer.parseInt(auth.substring(colon+1)); + } else { + host = auth; + port = uri.getPort(); + if (port < 1) { + if ("http".equals(uri.getScheme())) { + port = 80; + } else if ("https".equals(uri.getScheme())) { + port = 443; + } else { + throw new RuntimeException ("Invalid scheme provided for URI " + uri); + } + } + } + auth=null; + } + } + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public String getUserInfo() { + return auth; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Holder.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Holder.java new file mode 100644 index 00000000..dd313178 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Holder.java @@ -0,0 +1,46 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.util; + +/** + * Use to set Variables outside of Anonymous classes. + * @author Jonathan + * + * @param <T> + */ +public class Holder<T> { + private T value; + public Holder(T t) { + value = t; + } + public T set(T t) { + value = t; + return t; + } + + public T get() { + return value; + } + public String toString() { + return value.toString(); + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/JsonOutputStream.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/JsonOutputStream.java new file mode 100644 index 00000000..933877d3 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/JsonOutputStream.java @@ -0,0 +1,89 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.util; + +import java.io.IOException; +import java.io.OutputStream; + +public class JsonOutputStream extends OutputStream { + private static final byte[] TWO_SPACE = " ".getBytes(); + private OutputStream os; + private boolean closeable; + private int indent = 0; + private int prev,ret=0; + + public JsonOutputStream(OutputStream os) { + // Don't close these, or dire consequences. + closeable = !os.equals(System.out) && !os.equals(System.err); + this.os = os; + } + + @Override + public void write(int b) throws IOException { + if (ret=='\n') { + ret = 0; + if (prev!=',' || (b!='{' && b!='[')) { + os.write('\n'); + for (int i=0;i<indent;++i) { + os.write(TWO_SPACE); + } + } + } + switch(b) { + case '{': + case '[': + ret = '\n'; + ++indent; + break; + case '}': + case ']': + --indent; + os.write('\n'); + for (int i=0;i<indent;++i) { + os.write(TWO_SPACE); + } + break; + case ',': + ret = '\n'; + break; + + } + os.write(b); + prev = b; + } + public void resetIndent() { + indent = 1; + } + + @Override + public void flush() throws IOException { + os.flush(); + } + + @Override + public void close() throws IOException { + if (closeable) { + os.close(); + } + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Log.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Log.java new file mode 100644 index 00000000..ab75d0c6 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Log.java @@ -0,0 +1,37 @@ +/** + * ============LICENSE_START==================================================== + * Log + * =========================================================================== + * Copyright (c) May 11, 2020 Gathman Systems. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + */ +package org.onap.ccsdk.apps.cadi.util; + +/** + * A basic log interface used to Facade into Log Libraries used locally. + * + * @author Jonathan + * + */ +public interface Log { + enum Type {debug,info,warn,error,trace}; + public void log(Log.Type type, Object ... o); + + public final static Log NULL = new Log() { + @Override + public void log(Log.Type type, Object ... o) { + } + }; +}
\ No newline at end of file diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/MaskFormatException.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/MaskFormatException.java new file mode 100644 index 00000000..0f0f15ac --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/MaskFormatException.java @@ -0,0 +1,31 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.util; + +@SuppressWarnings("serial") +public class MaskFormatException extends Exception { + + public MaskFormatException(String string) { + super(string); + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/MyConsole.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/MyConsole.java new file mode 100644 index 00000000..2ab28e19 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/MyConsole.java @@ -0,0 +1,28 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.util; + +public interface MyConsole { + public String readLine(String fmt, Object ... args); + public char[] readPassword(String fmt, Object ... args); + public void printf(String fmt, Object ...args); +}
\ No newline at end of file diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/NetMask.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/NetMask.java new file mode 100644 index 00000000..3446e258 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/NetMask.java @@ -0,0 +1,99 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.util; + +/* + * NetMask - a class to quickly validate whether a given IP is part of a mask, as defined by bytes or standard String format. + * + * Needs the IPV6 Mask Builder. + */ +public class NetMask { + private long mask; + + public NetMask(byte[] inBytes) { + mask = derive(inBytes); + } + + public NetMask(String string) throws MaskFormatException { + mask = derive(string,true); + } + + public boolean isInNet(byte[] inBytes) { + long addr = derive(inBytes); + return (mask & addr) == addr; + } + + public boolean isInNet(String str) { + long addr; + try { + addr = derive(str,false); + return (mask & addr) == addr; + } catch (MaskFormatException e) { + // will not hit this code; + return false; + } + } + + public static long derive(byte[] inBytes) { + long addr = 0L; + int offset = inBytes.length*8; + for (int i=0;i<inBytes.length;++i) { + addr&=(inBytes[i]<<offset); + offset-=8; + } + return addr; + } + + public static long derive(String str, boolean check) throws MaskFormatException { + long rv=0L; + int idx=str.indexOf(':'); + int slash = str.indexOf('/'); + + if (idx<0) { // Not IPV6, so it's IPV4... Is there a mask of 123/254? + idx=str.indexOf('.'); + int offset = 24; + int end = slash>=0?slash:str.length(); + int bits = slash>=0?Integer.parseInt(str.substring(slash+1)):32; + if (check && bits>32) { + throw new MaskFormatException("Invalid Mask Offset in IPV4 Address"); + } + int prev = 0; + long lbyte; + while (prev<end) { + if (idx<0) { + idx = end; + } + lbyte = Long.parseLong(str.substring(prev, idx)); + if (check && (lbyte>255 || lbyte<0)) { + throw new MaskFormatException("Invalid Byte in IPV4 Address"); + } + rv|=lbyte<<offset; + prev = ++idx; + idx=str.indexOf('.',prev); + offset-=8; + } + rv|=0x00000000FFFFFFFFL>>bits; + } + return rv; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Pool.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Pool.java new file mode 100644 index 00000000..419be8c3 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Pool.java @@ -0,0 +1,453 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +/* + * Pool + * + * Author: Jonathan + * 5/27/2011 + */ +package org.onap.ccsdk.apps.cadi.util; + +import java.util.Iterator; +import java.util.LinkedList; + +import org.onap.ccsdk.apps.cadi.CadiException; + +/** + * This Class pools on an As-Needed-Basis any particular kind of class, which is + * quite suitable for expensive operations. + * + * The user calls "get" on a Pool, and if a waiting resource (T) is available, + * it will be returned. Otherwise, one will be created with the "Creator" class + * (must be defined for (T)). + * + * You can Prime the instances to avoid huge startup costs + * + * The returned "Pooled" object simply has to call "done()" and the object is + * returned to the pool. If the developer does not return the object, a memory + * leak does not occur. There are no references to the object once "get" is + * called. However, the developer who does not return the object when done + * obviates the point of the pool, as new Objects are created in place of the + * Object not returned when another call to "get" is made. + * + * There is a cushion of extra objects, currently defaulted to MAX_RANGE. If the + * items returned become higher than the MAX_RANGE, the object is allowed to go + * out of scope, and be cleaned up. the default can be changed on a per-pool + * basis. + * + * Class revamped for CadiExceptions and Access logging 10/4/2017 + * + * @author Jonathan + * + * @param <T> + */ +public class Pool<T> { + /** + * This is a constant which specified the default maximum number of unused + * objects to be held at any given time. + */ + public static final int MAX_RANGE = 6; // safety + + /** + * Maximum objects, in use or waiting + */ + public static final int MAX_OBJECTS = 20; // assumption for thread + + /** + * only Simple List needed. + * + * NOTE TO MAINTAINERS: THIS OBJECT DOES IT'S OWN SYNCHRONIZATION. All + * changes that touch list must account for correctly synchronizing list. + */ + private LinkedList<Pooled<T>> list; + + /** + * keep track of how many elements are currently available to use, to avoid asking list. + */ + private int count; + + /** + * how many objects have been asked for, but not returned or tossed + */ + private int used; + + /** + * Actual MAX number of spares allowed to hang around. Can be set to + * something besides the default MAX_RANGE. + */ + private int max_range = MAX_RANGE; + + /** + * Actual MAX number of Objects both in use, or waiting. + * This does not actually affect the Pool, because the objects, once they leave the pool, are not known until + * they are put back with done (offer). It only affects the "overLimit()" function. + * + * Important... this information is only valid if PooledObjects call "done()" or "toss()". + */ + private int max_objects = MAX_OBJECTS; + + /** + * The Creator for this particular pool. It must work for type T. + */ + private Creator<T> creator; + + private Log logger; + + /** + * Create a new Pool, given the implementation of Creator<T>, which must be + * able to create/destroy T objects at will. + * + * @param creator + */ + public Pool(Creator<T> creator) { + count = used = 0; + this.creator = creator; + list = new LinkedList<>(); + logger = Log.NULL; + } + + /** + * Attach Pool Logging activities to any other Logging Mechanism. + * @param logger + */ + public void setLogger(Log logger) { + this.logger = logger; + // Also reset existing Pooled objects + for(Pooled<?> p : list) { + if(p.content instanceof LogAware) { + ((LogAware)p.content).setLog(logger); + } else { + break; + } + } + } + + public void log(Log.Type type, Object ...objects) { + logger.log(type,objects); + } + + /** + * Preallocate a certain number of T Objects. Useful for services so that + * the first transactions don't get hit with all the Object creation costs + * + * It is assumed that priming also means that it is the minimum desired available resources. Therefore, + * max_range is set to prime, if less than current max_range, if it is default. + * + * @param lt + * @param prime + * @throws CadiException + */ + public Pool<T> prime(int prime) throws CadiException { + if(max_range == MAX_RANGE && prime<max_range) { + max_range = prime; + } + for (int i = 0; i < prime; ++i) { + Pooled<T> pt = new Pooled<T>(creator.create(), this); + synchronized (list) { + list.addFirst(pt); + ++count; + ++used; + } + } + return this; + } + + /** + * Destroy and remove all remaining objects. This is valuable for closing + * down all Allocated objects cleanly for exiting. It is also a good method + * for removing objects when, for instance, all Objects are invalid because + * of broken connections, etc. + * + * Use in conjunction with setMaxRange to no longer store objects, i.e. + * + * pool.setMaxRange(0).drain(); + */ + public synchronized void drain() { + while(list.size()>0) { + Pooled<T> pt = list.remove(); + --used; + String name = pt.content.toString(); + creator.destroy(pt.content); + logger.log(Log.Type.debug,"Pool destroyed", name); + } + count = 0; + } + + /** + * This is the essential function for Pool. Get an Object "T" inside a + * "Pooled<T>" object. If there is a spare Object, then use it. If not, then + * create and pass back. + * + * This one uses a Null LogTarget + * + * IMPORTANT: When the use of this object is done (and the object is still + * in a valid state), then "done()" should be called immediately to allow + * the object to be reused. That is the point of the Pool... + * + * If the Object is in an invalid state, then "toss()" should be used so the + * Pool doesn't pass on invalid objects to others. + * + * @param lt + * @return + * @throws CadiException + */ + public Pooled<T> get() throws CadiException { + Pooled<T> pt; + synchronized (list) { + pt = list.pollLast(); + } + if (pt == null) { + pt = new Pooled<T>(creator.create(), this); + ++used; + } else { + --count; + creator.reuse(pt.content); + } + return pt; + } + + /** + * This function will validate whether the Objects are still in a usable + * state. If not, they are tossed from the Pool. This is valuable to have + * when Remote Connections go down, and there is a question on whether the + * Pooled Objects are still functional. + * + * @return + */ + public boolean validate() { + boolean rv = true; + synchronized (list) { + for (Iterator<Pooled<T>> iter = list.iterator(); iter.hasNext();) { + Pooled<T> t = iter.next(); + if (!creator.isValid(t.content)) { + rv = false; + t.toss(); + iter.remove(); + } + } + } + return rv; + } + + /** + * This is an internal method, used only by the Internal Pooled<T> class. + * + * The Pooled<T> class "offers" it's Object back after use. It is an + * "offer", because Pool will simply destroy and remove the object if it has + * more than enough spares. + * + * @param lt + * @param used + * @return + */ + // Used only by Pooled<T> + private boolean offer(Pooled<T> usedP) { + if (count < max_range) { + synchronized (list) { + list.addFirst(usedP); + ++count; + } + logger.log(Log.Type.trace,"Pool recovered ", creator); + } else { + destroy(usedP.content); + } + return false; + } + + /** + * Destroy, using Creator's specific semantics, the Object, and decrement "used" + * + * @param t + */ + private void destroy(T t) { + creator.destroy(t); + synchronized (list) { + --used; + } + logger.log(Log.Type.debug,"Pool destroyed ", creator); + } + + /** + * The Creator Interface give the Pool the ability to Create, Destroy and + * Validate the Objects it is maintaining. Thus, it is a specially written + * Implementation for each type. + * + * @author Jonathan + * + * @param <T> + */ + public interface Creator<T> { + public T create() throws CadiException; + + public void destroy(T t); + + public boolean isValid(T t); + + public void reuse(T t); + } + + /** + * Pooled Classes can be "Log Aware", which means they can tie into the same + * Logging element that the Pool is using. To do this, the Object must implement "LogAware" + * + * @author Jonathan + * + */ + public interface LogAware { + public void setLog(Log log); + } + + /** + * The "Pooled<T>" class is the transient class that wraps the actual Object + * T for API use/ It gives the ability to return ("done()", or "toss()") the + * Object to the Pool when processing is finished. + * + * For Safety, i.e. to avoid memory leaks and invalid Object States, there + * is a "finalize" method. It is strictly for when coder forgets to return + * the object, or perhaps hasn't covered the case during Exceptions or + * Runtime Exceptions with finally (preferred). This should not be + * considered normal procedure, as finalize() is called at an undetermined + * time during garbage collection, and is thus rather useless for a Pool. + * However, we don't want Coding Mistakes to put the whole program in an + * invalid state, so if something happened such that "done()" or "toss()" + * were not called, the resource is still cleaned up as well as possible. + * + * @author Jonathan + * + * @param <T> + */ + public static class Pooled<T> { + public final T content; + private Pool<T> pool; + + /** + * Create the Wrapping Object Pooled<T>. + * + * @param t + * @param pool + * @param logTarget + */ + public Pooled(T t, Pool<T> pool) { + content = t; + if(t instanceof LogAware) { + ((LogAware)t).setLog(pool.logger); + } + this.pool = pool; + } + + /** + * This is the key API for the Pool, as calling "done()" offers this + * object back to the Pool for reuse. + * + * Do not use the Pooled<T> object again after calling "done()". + */ + public void done() { + if (pool != null) { + pool.offer(this); + } + } + + /** + * The user of the Object may discover that the Object t is no longer in + * a valid state. Don't put Garbage back in the Refrigerator... Toss it, + * if it's no longer valid. + * + * toss() is also used for draining the Pool, etc. + * + * toss() will attempt to destroy the Object by using the Creator + * Interface. + * + */ + public void toss() { + if (pool != null) { + pool.destroy(content); + } + // Don't allow finalize to put it back in. + pool = null; + } + + /** + * Just in case someone neglected to offer back object... Do not rely on + * this, as there is no specific time when finalize is called, which + * rather defeats the purpose of a Pool. + */ + @Override + protected void finalize() throws Throwable { + if (pool != null) { + done(); + pool = null; + } + } + + @Override + public String toString() { + return content.toString(); + } + } + + /** + * Set a Max Range for numbers of spare objects waiting to be used. + * + * No negative numbers are allowed + * + * Use in conjunction with drain to no longer store objects, i.e. + * + * pool.setMaxRange(0).drain(); + * + * @return + */ + public Pool<T> setMaxRange(int max_range) { + // Do not allow negative numbers + this.max_range = Math.max(0, max_range); + return this; + } + + /** + * Set a Max Range for numbers of spare objects waiting to be used. + * + * No negative numbers are allowed + * + * @return + */ + public Pool<T> setMaxObjects(int max_objects) { + // Do not allow negative numbers + this.max_objects = Math.max(0, max_objects); + return this; + } + + /** + * return whether objects in use or waiting are beyond max allowed + * + * Pool does not actually stop new creations, but allows this to be used by + * other entities to limit number of creations of expensive Objects, like + * Thread Pooling + * + */ + public boolean tooManyObjects() { + return used > max_objects; + } + + public String toString() { + return String.format("Pool: count(%d), used(%d), max_range(%d), max_objects(%d)", + count, used,max_range,max_objects); + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Split.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Split.java new file mode 100644 index 00000000..a5f65789 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Split.java @@ -0,0 +1,123 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.util; + +/** + * Split by Char, optional Trim + * + * Note: Copied from Inno to avoid linking issues. + * Note: I read the String split and Pattern split code, and we can do this more efficiently for a single Character + * + * 8/20/2015 + */ + +public class Split { + private static final String[] EMPTY = new String[0]; + + public static String[] split(char c, String value) { + if (value==null) { + return EMPTY; + } + + return split(c,value,0,value.length()); + } + + public static String[] split(char c, String value, int start, int end) { + if (value==null) { + return EMPTY; + } + + // Count items to preallocate Array (memory alloc is more expensive than counting twice) + int count,idx; + for (count=1,idx=value.indexOf(c,start);idx>=0 && idx<end;idx=value.indexOf(c,++idx),++count); + String[] rv = new String[count]; + if (count==1) { + rv[0]=value.substring(start,end); + } else { + int last=0; + count=-1; + for (idx=value.indexOf(c,start);idx>=0 && idx<end;idx=value.indexOf(c,idx)) { + rv[++count]=value.substring(last,idx); + last = ++idx; + } + rv[++count]=value.substring(last,end); + } + return rv; + } + + public static String[] splitTrim(char c, String value, int start, int end) { + if (value==null) { + return EMPTY; + } + + // Count items to preallocate Array (memory alloc is more expensive than counting twice) + int count,idx; + for (count=1,idx=value.indexOf(c,start);idx>=0 && idx<end;idx=value.indexOf(c,++idx),++count); + String[] rv = new String[count]; + if (count==1) { + rv[0]=value.substring(start,end).trim(); + } else { + int last=start; + count=-1; + for (idx=value.indexOf(c,start);idx>=0 && idx<end;idx=value.indexOf(c,idx)) { + rv[++count]=value.substring(last,idx).trim(); + last = ++idx; + } + rv[++count]=value.substring(last,end).trim(); + } + return rv; + } + + public static String[] splitTrim(char c, String value) { + if (value==null) { + return EMPTY; + } + return splitTrim(c,value,0,value.length()); + } + + public static String[] splitTrim(char c, String value, int size) { + if (value==null) { + return EMPTY; + } + + int idx; + String[] rv = new String[size]; + if (size==1) { + rv[0]=value.trim(); + } else { + int last=0; + int count=-1; + size-=2; + for (idx=value.indexOf(c);idx>=0 && count<size;idx=value.indexOf(c,idx)) { + rv[++count]=value.substring(last,idx).trim(); + last = ++idx; + } + if (idx>0) { + rv[++count]=value.substring(last,idx).trim(); + } else { + rv[++count]=value.substring(last).trim(); + } + } + return rv; + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/SubStandardConsole.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/SubStandardConsole.java new file mode 100644 index 00000000..25876eb2 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/SubStandardConsole.java @@ -0,0 +1,70 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +// Substandard, because System.in doesn't do Passwords.. +public class SubStandardConsole implements MyConsole { + private final static char[] BLANK = new char[0]; + private final BufferedReader br; + + public SubStandardConsole() { + br = new BufferedReader(new InputStreamReader(System.in)); + } + + @Override + public String readLine(String fmt, Object... args) { + String rv; + try { + System.out.printf(fmt,args); + rv = br.readLine(); + if (args.length==1 && rv.length()==0) { + rv = args[0].toString(); + } + } catch (IOException e) { + System.err.println("uh oh..."); + rv = ""; + } + return rv; + } + + @Override + public char[] readPassword(String fmt, Object... args) { + try { + System.out.printf(fmt,args); + String response = br.readLine(); + return response==null?BLANK:response.toCharArray(); + + } catch (IOException e) { + System.err.println("uh oh..."); + return BLANK; + } + } + + @Override + public void printf(String fmt, Object... args) { + System.out.printf(fmt, args); + } +}
\ No newline at end of file diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/TheConsole.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/TheConsole.java new file mode 100644 index 00000000..17347604 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/TheConsole.java @@ -0,0 +1,47 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.util; + +public class TheConsole implements MyConsole { + @Override + public String readLine(String fmt, Object... args) { + String rv = System.console().readLine(fmt, args); + if (args.length>0 && args[0]!=null && rv.length()==0) { + rv = args[0].toString(); + } + return rv; + } + + @Override + public char[] readPassword(String fmt, Object... args) { + return System.console().readPassword(fmt, args); + } + + public static boolean implemented() { + return System.console()!=null; + } + + @Override + public void printf(String fmt, Object... args) { + System.console().printf(fmt, args); + } +}
\ No newline at end of file diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Timing.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Timing.java new file mode 100644 index 00000000..69db6669 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Timing.java @@ -0,0 +1,27 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ +package org.onap.ccsdk.apps.cadi.util; + +public class Timing { + public static float millis(final long start) { + return (System.nanoTime() - start) / 1000000f; + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/UserChainManip.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/UserChainManip.java new file mode 100644 index 00000000..13ddfe00 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/UserChainManip.java @@ -0,0 +1,77 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.util; + +import org.onap.ccsdk.apps.cadi.UserChain; + +public class UserChainManip { + /** + Build an element in the correct format for UserChain. + Format:<APP>:<ID>:<protocol>[:AS][,<APP>:<ID>:<protocol>]* + @see UserChain + */ + public static StringBuilder build(StringBuilder sb, String app, String id, UserChain.Protocol proto, boolean as) { + boolean mayAs; + if (!(mayAs=sb.length()==0)) { + sb.append(','); + } + sb.append(app); + sb.append(':'); + sb.append(id); + sb.append(':'); + sb.append(proto.name()); + if (as && mayAs) { + sb.append(":AS"); + } + return sb; + } + + public static String idToNS(String id) { + if (id==null) { + return ""; + } else { + StringBuilder sb = new StringBuilder(); + char c; + int end; + boolean first = true; + for (int idx = end = id.length()-1;idx>=0;--idx) { + if ((c = id.charAt(idx))=='@' || c=='.') { + if (idx<end) { + if (first) { + first = false; + } else { + sb.append('.'); + } + for (int i=idx+1;i<=end;++i) { + sb.append(id.charAt(i)); + } + } + end=idx-1; + if (c=='@') { + break; + } + } + } + return sb.toString(); + } + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Vars.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Vars.java new file mode 100644 index 00000000..0609efd9 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/util/Vars.java @@ -0,0 +1,120 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.util; + +import java.util.List; + +public class Vars { + /** + * Simplified Conversion based on typical use of getting AT&T style RESTful Error Messages + * @param text + * @param vars + * @return + */ + public static String convert(final String text, final List<String> vars) { + StringBuilder sb = new StringBuilder(); + Object[] array = new Object[vars.size()]; + convert(sb,text,vars.toArray(array)); + return sb.toString(); + } + /** + * Convert a format string with "%s" into AT&T RESTful Error %1 %2 (number) format + * If "holder" is passed in, it is built with full Message extracted (typically for Logging) + * @param holder + * @param text + * @param vars + * @return + */ + public static String convert(final StringBuilder holder, final String text, final Object ... vars) { + StringBuilder sb = null; + int idx,index=0,prev = 0; + + if (text.contains("%s")) { + sb = new StringBuilder(); + } + + StringBuilder[] sbs = new StringBuilder[] {sb,holder}; + boolean replace, clearIndex = false; + int c; + while ((idx=text.indexOf('%',prev))>=0) { + replace = false; + if (clearIndex) { + index=0; + } + if (sb!=null) { + sb.append(text,prev,idx); + } + if (holder!=null) { + holder.append(text,prev,idx); + } + + boolean go = true; + while (go) { + if (text.length()>++idx) { + switch(c=text.charAt(idx)) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + index *=10; + index +=(c-'0'); + clearIndex=replace=true; + continue; + case 's': + ++index; + replace = true; + continue; + default: + break; + } + } + prev = idx; + go=false; + if (replace) { + if (sb!=null) { + sb.append('%'); + sb.append(index); + } + if (index<=vars.length) { + if (holder!=null) { + holder.append(vars[index-1]); + } + } + } else { + for (StringBuilder s : sbs) { + if (s!=null) { + s.append("%"); + } + } + } + } + } + + if (sb!=null) { + sb.append(text,prev,text.length()); + } + if (holder!=null) { + holder.append(text,prev,text.length()); + } + + return sb==null?text:sb.toString(); + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/Action.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/Action.java new file mode 100644 index 00000000..879a140e --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/Action.java @@ -0,0 +1,37 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.wsse; + +/** + * Interface to specify an action deep within a parsing tree on a local object + * + * We use a Generic so as to be flexible on create what that object actually is. This is passed in at the + * root "parse" call of Match. Similar to a "Visitor" Pattern, this object is passed upon reaching the right + * point in a parse tree. + * + * @author Jonathan + * + * @param <OUTPUT> + */ +interface Action<OUTPUT> { + public boolean content(OUTPUT output, String text); +}
\ No newline at end of file diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/Match.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/Match.java new file mode 100644 index 00000000..bd80c21b --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/Match.java @@ -0,0 +1,130 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.wsse; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.XMLEvent; + +/** + * Match Class allows you to build an automatic Tree of StAX (or StAX like) + * Objects for frequent use. + * + * OBJECT is a type which you which to do some end Actions on, similar to a Visitor pattern, see Action + * + * Note: We have implemented with XReader and XEvent, rather than StAX for performance reasons. + * + * @see Action + * @see Match + * @see XEvent + * @see XReader + * + * @author Jonathan + * + * @param <OUTPUT> + */ +//@SuppressWarnings("restriction") +public class Match<OUTPUT> { + private QName qname; + private Match<OUTPUT>[] next; + private Match<OUTPUT> prev; + private Action<OUTPUT> action = null; + private boolean stopAfter; + private boolean exclusive; + + + @SafeVarargs + public Match(String ns, String name, Match<OUTPUT> ... next) { + this.qname = new QName(ns,name); + this.next = next; + stopAfter = exclusive = false; + for (Match<OUTPUT> m : next) { // add the possible tags to look for + if (!m.stopAfter)m.prev = this; + } + } + + public Match<OUTPUT> onMatch(OUTPUT output, XReader reader) throws XMLStreamException { + while (reader.hasNext()) { + XEvent event = reader.nextEvent(); + switch(event.getEventType()) { + case XMLEvent.START_ELEMENT: + QName e_qname = event.asStartElement().getName(); + //System.out.println("Start - " + e_qname); + boolean match = false; + for (Match<OUTPUT> m : next) { + if (e_qname.equals(m.qname)) { + match=true; + if (m.onMatch(output, reader)==null) { + return null; // short circuit Parsing + } + break; + } + } + if (exclusive && !match) // When Tag MUST be present, i.e. the Root Tag, versus info we're not interested in + return null; + break; + case XMLEvent.CHARACTERS: + //System.out.println("Data - " +event.asCharacters().getData()); + if (action!=null) { + if (!action.content(output,event.asCharacters().getData())) { + return null; + } + } + break; + case XMLEvent.END_ELEMENT: + //System.out.println("End - " + event.asEndElement().getName()); + if (event.asEndElement().getName().equals(qname)) { + return prev; + } + break; + case XMLEvent.END_DOCUMENT: + return null; // Exit Chain + } + } + return this; + } + + /** + * When this Matched Tag has completed, Stop parsing and end + * @return + */ + public Match<OUTPUT> stopAfter() { + stopAfter = true; + return this; + } + + /** + * Mark that this Object MUST be matched at this level or stop parsing and end + * + * @param action + * @return + */ + public Match<OUTPUT> exclusive() { + exclusive = true; + return this; + } + + public Match<OUTPUT> set(Action<OUTPUT> action) { + this.action = action; + return this; + } +}
\ No newline at end of file diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/WSSEParser.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/WSSEParser.java new file mode 100644 index 00000000..4f85fa5f --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/WSSEParser.java @@ -0,0 +1,83 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.wsse; + +import java.io.InputStream; + +import javax.xml.stream.XMLStreamException; + +import org.onap.ccsdk.apps.cadi.BasicCred; + + +/** + * WSSE Parser + * + * Read the User and Password from WSSE Formatted SOAP Messages + * + * This class uses StAX so that processing is stopped as soon as the Security User/Password are read into BasicCred, or the Header Ends + * + * This class is intended to be created once (or very few times) and reused as much as possible. + * + * It is as thread safe as StAX parsing is. + * + * @author Jonathan + */ +public class WSSEParser { + private static final String SOAP_NS = "http://schemas.xmlsoap.org/soap/envelope/"; + private static final String WSSE_NS = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"; + private Match<BasicCred> parseTree; + + public WSSEParser() { + // soap:Envelope/soap:Header/wsse:Security/wsse:UsernameToken/[wsse:Password&wsse:Username] + parseTree = new Match<BasicCred>(SOAP_NS,"root", // need a root level to start from... Doesn't matter what the tag is + new Match<BasicCred>(SOAP_NS,"Envelope", + new Match<BasicCred>(SOAP_NS,"Header", + new Match<BasicCred>(WSSE_NS,"Security", + new Match<BasicCred>(WSSE_NS,"UsernameToken", + new Match<BasicCred>(WSSE_NS,"Password").set(new Action<BasicCred>() { + public boolean content(BasicCred bc,String text) { + bc.setCred(text.getBytes()); + return true; + } + }), + new Match<BasicCred>(WSSE_NS,"Username").set(new Action<BasicCred>() { + public boolean content(BasicCred bc,String text) { + bc.setUser(text); + return true; + } + }) + ).stopAfter() // if found, end when UsernameToken ends (no further processing needed) + ) + ).stopAfter() // Stop Processing when Header Ends + ).exclusive()// Envelope must match Header, and no other. FYI, Body comes after Header short circuits (see above), so it's ok + ).exclusive(); // root must be Envelope + } + + public XMLStreamException parse(BasicCred bc, InputStream is) { + try { + parseTree.onMatch(bc, new XReader(is)); + return null; + } catch (XMLStreamException e) { + return e; + } + } +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/XEvent.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/XEvent.java new file mode 100644 index 00000000..909dd1d2 --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/XEvent.java @@ -0,0 +1,135 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.wsse; + +import javax.xml.namespace.QName; +import javax.xml.stream.events.XMLEvent; + +/** + * XEvent + * + * This mechanism mimics a minimal portion of StAX "XMLEvent", enough to work with minimal XReader. + * + * We implement the same interface, as much as minimally necessary, as XMLEvent for these small usages so as to + * be interchangeable in the future, if so desired + * + * @author Jonathan + * + */ +// @SuppressWarnings("restriction") +public abstract class XEvent { + + public abstract int getEventType(); + + public StartElement asStartElement() { + return (StartElement)this; + } + + public Characters asCharacters() { + return (Characters)this; + } + + public EndElement asEndElement() { + return (EndElement)this; + } + + public static abstract class NamedXEvent extends XEvent { + private QName qname; + + public NamedXEvent(QName qname) { + this.qname = qname; + } + + public QName getName() { + return qname; + } + } + public static class StartElement extends NamedXEvent { + + public StartElement(String ns, String tag) { + super(new QName(ns,tag)); + } + + @Override + public int getEventType() { + return XMLEvent.START_ELEMENT; + } + } + + public static class EndElement extends NamedXEvent { + public EndElement(String ns, String tag) { + super(new QName(ns,tag)); + } + + @Override + public int getEventType() { + return XMLEvent.END_ELEMENT; + } + } + + public static class Characters extends XEvent { + private String data; + + public Characters(String data) { + this.data = data; + } + @Override + public int getEventType() { + return XMLEvent.CHARACTERS; + } + + public String getData() { + return data; + } + } + + public static class StartDocument extends XEvent { + + @Override + public int getEventType() { + return XMLEvent.START_DOCUMENT; + } + + } + + public static class EndDocument extends XEvent { + + @Override + public int getEventType() { + return XMLEvent.END_DOCUMENT; + } + + } + public static class Comment extends XEvent { + public final String value; + public Comment(String value) { + this.value = value; + } + + @Override + public int getEventType() { + return XMLEvent.COMMENT; + } + + } + +} diff --git a/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/XReader.java b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/XReader.java new file mode 100644 index 00000000..3da8eddc --- /dev/null +++ b/cadi/core/src/main/java/org/onap/ccsdk/apps/cadi/wsse/XReader.java @@ -0,0 +1,427 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.ccsdk + * =========================================================================== + * Copyright (c) 2023 AT&T Intellectual Property. All rights reserved. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ + +package org.onap.ccsdk.apps.cadi.wsse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +import javax.xml.stream.XMLStreamException; + +/** + * XReader + * This class works similarly as StAX, except StAX has more behavior than is needed. That would be ok, but + * StAX also was Buffering in their code in such as way as to read most if not all the incoming stream into memory, + * defeating the purpose of pre-reading only the Header + * + * This Reader does no back-tracking, but is able to create events based on syntax and given state only, leaving the + * Read-ahead mode of the InputStream up to the other classes. + * + * At this time, we only implement the important events, though if this is good enough, it could be expanded, perhaps to + * replace the original XMLReader from StAX. + * + * @author Jonathan + * + */ +// @SuppressWarnings("restriction") +public class XReader { + private XEvent curr,another; + private InputStream is; + private ByteArrayOutputStream baos; + private int state, count, last; + + private Stack<Map<String,String>> nsses; + + public XReader(InputStream is) { + this.is = is; + curr = another = null; + baos = new ByteArrayOutputStream(); + state = BEGIN_DOC; + count = 0; + nsses = new Stack<Map<String,String>>(); + } + + public boolean hasNext() throws XMLStreamException { + if (curr==null) { + curr = parse(); + } + return curr!=null; + } + + public XEvent nextEvent() { + XEvent xe = curr; + curr = null; + return xe; + } + + // + // State Flags + // + // Note: The State of parsing XML can be complicated. There are too many to cleanly keep in "booleans". Additionally, + // there are certain checks that can be better made with Bitwise operations within switches + // Keeping track of state this way also helps us to accomplish logic without storing any back characters except one + private final static int BEGIN_DOC= 0x000001; + private final static int DOC_TYPE= 0x000002; + private final static int QUESTION_F= 0x000004; + private final static int QUESTION = 0x000008; + private final static int START_TAG = 0x000010; + private final static int END_TAG = 0x000020; + private final static int VALUE= 0x000040; + private final static int COMMENT = 0x001000; + private final static int COMMENT_E = 0x002000; + private final static int COMMENT_D1 =0x010000; + private final static int COMMENT_D2 =0x020000; + private final static int COMMENT_D3 =0x040000; + private final static int COMMENT_D4 =0x080000; + // useful combined Comment states + private final static int IN_COMMENT=COMMENT|COMMENT_E|COMMENT_D1|COMMENT_D2; + private final static int COMPLETE_COMMENT = COMMENT|COMMENT_E|COMMENT_D1|COMMENT_D2|COMMENT_D3|COMMENT_D4; + + + private XEvent parse() throws XMLStreamException { + Map<String,String> nss = nsses.isEmpty()?null:nsses.peek(); + + XEvent rv; + if ((rv=another)!=null) { // "another" is a tag that may have needed to be created, but not + // immediately returned. Save for next parse. If necessary, this could be turned into + // a FIFO storage, but a single reference is enough for now. + another = null; // "rv" is now set for the Event, and will be returned. Set to Null. + } else { + boolean go = true; + int c=0; + + try { + while (go && (c=is.read())>=0) { + ++count; + switch(c) { + case '<': // Tag is opening + state|=~BEGIN_DOC; // remove BEGIN_DOC flag, this is possibly an XML Doc + XEvent cxe = null; + if (baos.size()>0) { // If there are any characters between tags, we send as Character Event + String chars = baos.toString().trim(); // Trim out WhiteSpace before and after + if (chars.length()>0) { // don't send if Characters were only whitespace + cxe = new XEvent.Characters(chars); + baos.reset(); + go = false; + } + } + last = c; // make sure "last" character is set for use in "ParseTag" + Tag t = parseTag(); // call subroutine to process the tag as a unit + String ns; + switch(t.state&(START_TAG|END_TAG)) { + case START_TAG: + nss = getNss(nss,t); // Only Start Tags might have NS Attributes + // Get any NameSpace elements from tag. If there are, nss will become + // a new Map with all the previous NSs plus the new. This provides + // scoping behavior when used with the Stack + // drop through on purpose + case END_TAG: + ns = t.prefix==null||nss==null?"":nss.get(t.prefix); // Get the namespace from prefix (if exists) + break; + default: + ns = ""; + } + if (ns==null) + throw new XMLStreamException("Invalid Namespace Prefix at " + count); + go = false; + switch(t.state) { // based on + case DOC_TYPE: + rv = new XEvent.StartDocument(); + break; + case COMMENT: + rv = new XEvent.Comment(t.value); + break; + case START_TAG: + rv = new XEvent.StartElement(ns,t.name); + nsses.push(nss); // Change potential scope for Namespace + break; + case END_TAG: + rv = new XEvent.EndElement(ns,t.name); + nss = nsses.pop(); // End potential scope for Namespace + break; + case START_TAG|END_TAG: // This tag is both start/end aka <myTag/> + rv = new XEvent.StartElement(ns,t.name); + if (last=='/')another = new XEvent.EndElement(ns,t.name); + } + if (cxe!=null) { // if there is a Character Event, it actually should go first. ow. + another = rv; // Make current Event the "another" or next event, and + rv = cxe; // send Character Event now + } + break; + case ' ': + case '\t': + case '\n': + if ((state&BEGIN_DOC)==BEGIN_DOC) { // if Whitespace before doc, just ignore + break; + } + // fallthrough on purpose + default: + if ((state&BEGIN_DOC)==BEGIN_DOC) { // if there is any data at the start other than XML Tag, it's not XML + throw new XMLStreamException("Parse Error: This is not an XML Doc"); + } + baos.write(c); // save off Characters + } + last = c; // Some processing needs to know what the last character was, aka Escaped characters... ex \" + } + } catch (IOException e) { + throw new XMLStreamException(e); // all errors parsing will be treated as XMLStreamErrors (like StAX) + } + if (c==-1 && (state&BEGIN_DOC)==BEGIN_DOC) { // Normally, end of stream is ok, however, we need to know if the + throw new XMLStreamException("Premature End of File"); // document isn't an XML document, so we throw exception if it + } // hasn't yet been determined to be an XML Doc + } + return rv; + } + + /** + * parseTag + * + * Parsing a Tag is somewhat complicated, so it's helpful to separate this process from the + * higher level Parsing effort + * @return + * @throws IOException + * @throws XMLStreamException + */ + private Tag parseTag() throws IOException, XMLStreamException { + Tag tag = null; + boolean go = true; + state = 0; + int c, quote=0; // If "quote" is 0, then we're not in a quote. We set ' (in pretag) or " in attribs accordingly to denote quoted + String prefix=null,name=null,value=null; + baos.reset(); + + while (go && (c=is.read())>=0) { + ++count; + if (quote!=0) { // If we're in a quote, we only end if we hit another quote of the same time, not preceded by \ + if (c==quote && last!='\\') { + quote=0; + } else { + baos.write(c); + } + } else if ((state&COMMENT)==COMMENT) { // similar to Quote is being in a comment + switch(c) { + case '-': + switch(state) { // XML has a complicated Quote set... <!-- --> ... we keep track if each has been met with flags. + case COMMENT|COMMENT_E: + state|=COMMENT_D1; + break; + case COMMENT|COMMENT_E|COMMENT_D1: + state|=COMMENT_D2; + baos.reset(); // clear out "!--", it's a Comment + break; + case COMMENT|COMMENT_E|COMMENT_D1|COMMENT_D2: + state|=COMMENT_D3; + baos.write(c); + break; + case COMMENT|COMMENT_E|COMMENT_D1|COMMENT_D2|COMMENT_D3: + state|=COMMENT_D4; + baos.write(c); + break; + } + break; + case '>': // Tag indicator has been found, do we have all the comment characters in line? + if ((state&COMPLETE_COMMENT)==COMPLETE_COMMENT) { + byte ba[] = baos.toByteArray(); + tag = new Tag(null,null, new String(ba,0,ba.length-2)); + baos.reset(); + go = false; + break; + } + // fall through on purpose + default: + state&=~(COMMENT_D3|COMMENT_D4); + if ((state&IN_COMMENT)!=IN_COMMENT) state&=~IN_COMMENT; // false alarm, it's not actually a comment + baos.write(c); + } + } else { // Normal Tag Processing loop + switch(c) { + case '?': + switch(state & (QUESTION_F|QUESTION)) { // Validate the state of Doc tag... <?xml ... ?> + case QUESTION_F: + state |= DOC_TYPE; + state &= ~QUESTION_F; + break; + case 0: + state |=QUESTION_F; + break; + default: + throw new IOException("Bad character [?] at " + count); + } + break; + case '!': + if (last=='<') { + state|=COMMENT|COMMENT_E; // likely a comment, continue processing in Comment Loop + } + baos.write(c); + break; + case '/': + state|=(last=='<'?END_TAG:(END_TAG|START_TAG)); // end tag indicator </xxx>, ,or both <xxx/> + break; + case ':': + prefix=baos.toString(); // prefix indicator + baos.reset(); + break; + case '=': // used in Attributes + name=baos.toString(); + baos.reset(); + state|=VALUE; + break; + case '>': // end the tag, which causes end of this subprocess as well as formulation of the found data + go = false; + // passthrough on purpose + case ' ': + case '\t': + case '\n': // white space indicates change in internal tag state, ex between name and between attributes + if ((state&VALUE)==VALUE) { + value = baos.toString(); // we're in VALUE state, add characters to Value + } else if (name==null) { + name = baos.toString(); // we're in Name state (default) add characters to Name + } + baos.reset(); // we've assigned chars, reset buffer + if (name!=null) { // Name is not null, there's a tag in the offing here... + Tag t = new Tag(prefix,name,value); + if (tag==null) { // Set as the tag to return, if not exists + tag = t; + } else { // if we already have a Tag, then we'll treat this one as an attribute + tag.add(t); + } + } + prefix=name=value=null; // reset these values in case we loop for attributes. + break; + case '\'': // is the character one of two kinds of quote? + case '"': + if (last!='\\') { + quote=c; + break; + } + // Fallthrough ok + default: + baos.write(c); // write any unprocessed bytes into buffer + + } + } + last = c; + } + int type = state&(DOC_TYPE|COMMENT|END_TAG|START_TAG); // get just the Tag states and turn into Type for Tag + if (type==0) { + type=START_TAG; + } + if (tag!=null) { + tag.state|=type; // add the appropriate Tag States + } + return tag; + } + + /** + * getNSS + * + * If the tag contains some Namespace attributes, create a new nss from the passed in one, copy all into it, then add + * This provides Scoping behavior + * + * if Nss is null in the first place, create an new nss, so we don't have to deal with null Maps. + * + * @param nss + * @param t + * @return + */ + private Map<String, String> getNss(Map<String, String> nss, Tag t) { + Map<String,String> newnss = null; + if (t.attribs!=null) { + for (Tag tag : t.attribs) { + if ("xmlns".equals(tag.prefix)) { + if (newnss==null) { + newnss = new HashMap<>(); + if (nss!=null)newnss.putAll(nss); + } + newnss.put(tag.name, tag.value); + } + } + } + //return newnss==null?(nss==null?new HashMap<String,String>():nss):newnss; + if (newnss==null) { + if (nss==null) { + newnss = new HashMap<>(); + } else { + newnss = nss; + } + } + return newnss; + } + + /** + * The result of the parseTag method + * + * Data is split up into prefix, name and value portions. "Tags" with Values that are inside a Tag are known in XLM + * as Attributes. + * + * @author Jonathan + * + */ + public class Tag { + public int state; + public String prefix,name,value; + public List<Tag> attribs; + + public Tag(String prefix, String name, String value) { + this.prefix = prefix; + this.name = name; + this.value = value; + attribs = null; + } + + /** + * add an attribute + * Not all tags need attributes... lazy instantiate to save time and memory + * @param tag + */ + public void add(Tag attrib) { + if (attribs == null) { + attribs = new ArrayList<>(); + } + attribs.add(attrib); + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + if (prefix!=null) { + sb.append(prefix); + sb.append(':'); + } + sb.append(name==null?"!!ERROR!!":name); + + char quote = ((state&DOC_TYPE)==DOC_TYPE)?'\'':'"'; + if (value!=null) { + sb.append('='); + sb.append(quote); + sb.append(value); + sb.append(quote); + } + return sb.toString(); + } + } + +} |