From bc3fa191c1226389f4a660f0130a1642980e6dd9 Mon Sep 17 00:00:00 2001 From: Stelios Voutsinas Date: Wed, 24 Jul 2024 04:49:19 -0700 Subject: [PATCH] QueryJobManager to use IdentitytManager from AuthenticationUtil & upgrade to uws-server lib (1.2.21) --- CHANGELOG.md | 9 ++ build.gradle | 2 +- .../opencadc/tap/impl/AuthenticatorImpl.java | 152 ++++++++++++++++++ .../org/opencadc/tap/ws/QueryJobManager.java | 11 +- 4 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/opencadc/tap/impl/AuthenticatorImpl.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cdda6a..f75c212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ Find changes for the upcoming release in the project's [changelog.d](https://git + +## 2.4.5 (2024-07-24) + +### Changed + +- Changed QueryJobManager to use the IdentityManager available via the AuthenticationUtil class (OpenID in our case) +- Added deprecated AuthenticatorImpl, this is only useful in case this version of TAP is used with the old Auth params/implementations (Unlikely) +- Upgrade version of uws-server to 1.2.21 + ## 2.4.4 (2024-07-23) diff --git a/build.gradle b/build.gradle index f5f3c27..1c50d54 100644 --- a/build.gradle +++ b/build.gradle @@ -54,7 +54,7 @@ dependencies { implementation 'org.opencadc:cadc-tap-server-oracle:1.2.11' implementation 'org.opencadc:cadc-util:1.11.2' implementation 'org.opencadc:cadc-uws:1.0.5' - implementation 'org.opencadc:cadc-uws-server:1.2.20' + implementation 'org.opencadc:cadc-uws-server:1.2.21' implementation 'org.opencadc:cadc-vosi:1.4.6' // Switch out this to use any supported database instead of PostgreSQL. diff --git a/src/main/java/org/opencadc/tap/impl/AuthenticatorImpl.java b/src/main/java/org/opencadc/tap/impl/AuthenticatorImpl.java new file mode 100644 index 0000000..22374fa --- /dev/null +++ b/src/main/java/org/opencadc/tap/impl/AuthenticatorImpl.java @@ -0,0 +1,152 @@ +package org.opencadc.tap.impl; + +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpRequest; +import java.net.URI; +import java.security.AccessControlException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import javax.security.auth.Subject; +import javax.security.auth.x500.X500Principal; + +import ca.nrc.cadc.auth.Authenticator; +import ca.nrc.cadc.auth.AuthMethod; +import ca.nrc.cadc.auth.AuthorizationTokenPrincipal; +import ca.nrc.cadc.auth.HttpPrincipal; +import ca.nrc.cadc.auth.NumericPrincipal; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import org.apache.log4j.Logger; + +/** + * @deprecated This class is deprecated and will be removed in future releases. + * The TAP Service now uses IdentityManager for authentication, available in the opencadc library + * + * @author cbanek + */ +@Deprecated +public class AuthenticatorImpl implements Authenticator +{ + private static final Logger log = Logger.getLogger(AuthenticatorImpl.class); + + // Size of the token cache is read from the maxTokenCache property, with + // a default of 1000 tokens cached. + private static final int maxTokenCache = Integer.getInteger("maxTokenCache", 1000); + + private static final String gafaelfawr_url = System.getProperty("gafaelfawr_url"); + + private static final HttpClient client = HttpClient.newHttpClient(); + + private static final ConcurrentHashMap tokenCache = new ConcurrentHashMap<>(); + + private final class TokenInfo + { + public final String username; + public final int uid; + + public TokenInfo(String username, int uid) + { + this.username = username; + this.uid = uid; + } + } + + public AuthenticatorImpl() + { + } + + public Subject validate(Subject subject) throws AccessControlException { + log.debug("Subject to augment starts as: " + subject); + + // Check if the cache is too big, and if so, clear it out. + if (tokenCache.size() > maxTokenCache) { + tokenCache.clear(); + } + + List addedPrincipals = new ArrayList(); + AuthorizationTokenPrincipal tokenPrincipal = null; + + for (Principal principal : subject.getPrincipals()) { + if (principal instanceof AuthorizationTokenPrincipal) { + tokenPrincipal = (AuthorizationTokenPrincipal) principal; + TokenInfo tokenInfo = null; + + for (int i = 1; i < 5 && tokenInfo == null; i++) { + try { + tokenInfo = getTokenInfo(tokenPrincipal.getHeaderValue()); + } catch (IOException|InterruptedException e) { + log.warn("Exception thrown while getting info from Gafaelfawr"); + log.warn(e); + } + } + + if (tokenInfo != null) { + X500Principal xp = new X500Principal("CN=" + tokenInfo.username); + addedPrincipals.add(xp); + + HttpPrincipal hp = new HttpPrincipal(tokenInfo.username); + addedPrincipals.add(hp); + + UUID uuid = new UUID(0L, (long) tokenInfo.uid); + NumericPrincipal np = new NumericPrincipal(uuid); + addedPrincipals.add(np); + } + else { + log.error("Gave up retrying user-info requests to Gafaelfawr"); + } + } + } + + if (tokenPrincipal != null) { + subject.getPrincipals().remove(tokenPrincipal); + } + + subject.getPrincipals().addAll(addedPrincipals); + subject.getPublicCredentials().add(AuthMethod.TOKEN); + + log.debug("Augmented subject is " + subject); + return subject; + } + + // Here we could check the token again, but gafaelfawr should be + // doing that for us already by the time it gets to us. So for + // this layer, we just let this go through. + public Subject augment(Subject subject) { + return subject; + } + + private TokenInfo getTokenInfo(String token) throws IOException, InterruptedException { + // If the request has gotten this far, the token has already + // been checked upstream, so we know it's valid, we just need + // to determine the uid and the username. + if (!tokenCache.containsKey(token)) { + HttpRequest request = HttpRequest.newBuilder(URI.create(gafaelfawr_url)) + .header("Accept", "application/json") + .header("Authorization", token) + .build(); + + HttpResponse response = client.send(request, BodyHandlers.ofString()); + String body = response.body(); + + Gson gson = new Gson(); + JsonObject authData = gson.fromJson(body, JsonObject.class); + String username = authData.getAsJsonPrimitive("username").getAsString(); + int uid = authData.getAsJsonPrimitive("uid").getAsInt(); + + // Insert the info into the cache here since we retrieved it. + tokenCache.put(token, new TokenInfo(username, uid)); + } + + return tokenCache.get(token); + } +} diff --git a/src/main/java/org/opencadc/tap/ws/QueryJobManager.java b/src/main/java/org/opencadc/tap/ws/QueryJobManager.java index 99d5aa1..45d95c4 100644 --- a/src/main/java/org/opencadc/tap/ws/QueryJobManager.java +++ b/src/main/java/org/opencadc/tap/ws/QueryJobManager.java @@ -9,6 +9,13 @@ import ca.nrc.cadc.uws.server.SimpleJobManager; import ca.nrc.cadc.uws.server.ThreadPoolExecutor; +import ca.nrc.cadc.auth.AuthenticationUtil; +import ca.nrc.cadc.auth.IdentityManager; +import ca.nrc.cadc.uws.server.JobPersistence; +import ca.nrc.cadc.uws.server.RandomStringGenerator; +import ca.nrc.cadc.uws.server.RequestPathJobManager; +import org.apache.log4j.Logger; + /** * @author pdowler @@ -22,7 +29,9 @@ public class QueryJobManager extends SimpleJobManager { public QueryJobManager() { super(); - PostgresJobPersistence jobPersist = new PostgresJobPersistence(new X500IdentityManager()); + IdentityManager im = AuthenticationUtil.getIdentityManager(); + // persist UWS jobs to PostgreSQL using default jdbc/uws connection pool + JobPersistence jobPersist = new PostgresJobPersistence(new RandomStringGenerator(16), im, true); // max threads: 6 == number of simultaneously running async queries (per // web server), plus sync queries, plus VOSI-tables queries