-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Rubin IdentityManager implementation
- Loading branch information
Showing
2 changed files
with
244 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
237 changes: 237 additions & 0 deletions
237
tap/src/main/java/ca/nrc/cadc/sample/RubinIdentityManagerImpl.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |