Skip to content

Commit

Permalink
Add Rubin IdentityManager implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
stvoutsin committed Jul 24, 2024
1 parent 8d5174b commit cff9606
Show file tree
Hide file tree
Showing 2 changed files with 244 additions and 0 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ Find changes for the upcoming release in the project's [changelog.d](https://git

<!-- scriv-insert-here -->

<a id='changelog-1.18.5'></a>
## 1.18.5 (2024-07-24)

### Fixed

- Introduce RubinIdentityManagerImpl, extends IdentityManager which replaces the deprecated Authenticator

<a id='changelog-1.18.4'></a>
## 1.18.4 (2024-07-19)

Expand Down
237 changes: 237 additions & 0 deletions tap/src/main/java/ca/nrc/cadc/sample/RubinIdentityManagerImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
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<String,TokenInfo> 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<URI> getSecurityMethods() {
return SEC_METHODS;
}
private static final Set<URI> SEC_METHODS;

static {
Set<URI> 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<Principal> addedPrincipals = new ArrayList<Principal>();
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<String> 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<NumericPrincipal> 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<HttpPrincipal> ps = subject.getPrincipals(HttpPrincipal.class);
if (!ps.isEmpty()) {
return ps.iterator().next().getName(); // kind of ugh
}

// default
Set<Principal> ps2 = subject.getPrincipals();
if (!ps2.isEmpty()) {
return ps2.iterator().next().getName();
}
}

return null;
}
}

0 comments on commit cff9606

Please sign in to comment.