diff --git a/CHANGELOG.md b/CHANGELOG.md index 75018d2..dedfde8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,9 @@ Find changes for the upcoming release in the project's [changelog.d](https://git ### Fixed -- Introduce RubinIdentityManagerImpl, extends IdentityManager which replaces the deprecated Authenticator +- 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 ## 1.18.4 (2024-07-19) diff --git a/tap/build.gradle b/tap/build.gradle index 7a13089..69575f8 100644 --- a/tap/build.gradle +++ b/tap/build.gradle @@ -31,7 +31,7 @@ dependencies { implementation 'org.opencadc:cadc-tap-server-pg:[1.0.0,)' 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/tap/src/main/java/ca/nrc/cadc/sample/JobManager.java b/tap/src/main/java/ca/nrc/cadc/sample/JobManager.java index f85a881..5dcea5f 100644 --- a/tap/src/main/java/ca/nrc/cadc/sample/JobManager.java +++ b/tap/src/main/java/ca/nrc/cadc/sample/JobManager.java @@ -7,6 +7,10 @@ import ca.nrc.cadc.uws.server.impl.PostgresJobPersistence; 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; /** @@ -21,7 +25,9 @@ public class JobManager extends SimpleJobManager { public JobManager() { 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 diff --git a/tap/src/main/java/ca/nrc/cadc/sample/RubinIdentityManagerImpl.java b/tap/src/main/java/ca/nrc/cadc/sample/RubinIdentityManagerImpl.java deleted file mode 100644 index 3d66d74..0000000 --- a/tap/src/main/java/ca/nrc/cadc/sample/RubinIdentityManagerImpl.java +++ /dev/null @@ -1,237 +0,0 @@ -package ca.nrc.cadc.sample; -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.Set; -import java.util.TreeSet; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.Collections; - -import javax.security.auth.Subject; -import javax.security.auth.x500.X500Principal; - -import ca.nrc.cadc.auth.AuthMethod; -import ca.nrc.cadc.auth.AuthenticationUtil; -import ca.nrc.cadc.auth.Authenticator; -import ca.nrc.cadc.auth.AuthMethod; -import ca.nrc.cadc.auth.IdentityManager; -import ca.nrc.cadc.reg.Standards; - -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; - -/** - * Implementes the Authenticator for processing Gafaelfawr auth, - * and using it to authenticate against the TAP service. - * - * The token in the authorization header is used to make a call - * to Gafaelfawr to retrieve details such as the uid and uidNumber. - * - * @author cbanek - * @author stvoutsin - */ -public class RubinIdentityManagerImpl implements IdentityManager -{ - private static final Logger log = Logger.getLogger(RubinIdentityManagerImpl.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; - } - } - @Override - public Set getSecurityMethods() { - return SEC_METHODS; - } - private static final Set SEC_METHODS; - - static { - Set tmp = new TreeSet<>(); - tmp.add(Standards.SECURITY_METHOD_ANON); - tmp.add(Standards.SECURITY_METHOD_TOKEN); - SEC_METHODS = Collections.unmodifiableSet(tmp); - } - - public RubinIdentityManagerImpl() - { - } - - 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); - } - - - @Override - public Subject toSubject(Object owner) { - Subject ret = new Subject(); - if (owner != null) { - UUID uuid = null; - if (owner instanceof UUID) { - uuid = (UUID) owner; - } else if (owner instanceof String) { - String sub = (String) owner; - uuid = UUID.fromString(sub); - } else { - throw new RuntimeException("unexpected owner type: " + owner.getClass().getName() + " value: " + owner); - } - NumericPrincipal p = new NumericPrincipal(uuid); - - // effectively augment by using the current subject as a "cache" of known identities - Subject s = AuthenticationUtil.getCurrentSubject(); - if (s != null) { - for (Principal cp : s.getPrincipals()) { - if (AuthenticationUtil.equals(p, cp)) { - log.debug("[cache hit] caller Subject matches " + p + ": " + s); - ret.getPrincipals().addAll(s.getPrincipals()); - return ret; - } - } - } - - ret.getPrincipals().add(p); - // this is sufficient for some purposes, but not for output using toDisplayString (eg vospace node owner) - // TODO: use PosixMapperClient.augment() to try to add a PosixPrincipal and infer an HttpPrincipal? - } - return ret; - } - - @Override - public Object toOwner(Subject subject) { - // use NumericPrincipal aka OIDC sub for persistence - Set ps = subject.getPrincipals(NumericPrincipal.class); - if (ps.isEmpty()) { - return null; - } - return ps.iterator().next().getUUID().toString(); - } - - @Override - public String toDisplayString(Subject subject) { - if (subject != null) { - // prefer HttpPrincipal aka OIDC preferred_username for string output, eg logging - Set ps = subject.getPrincipals(HttpPrincipal.class); - if (!ps.isEmpty()) { - return ps.iterator().next().getName(); // kind of ugh - } - - // default - Set ps2 = subject.getPrincipals(); - if (!ps2.isEmpty()) { - return ps2.iterator().next().getName(); - } - } - - return null; - } -}