package edu.harvard.iq.dataverse.authorization.providers.shib;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import edu.harvard.iq.dataverse.EMailValidator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
public class ShibUtil {
private static final Logger logger = Logger.getLogger(ShibUtil.class.getCanonicalName());
/**
* @todo make this configurable? See
* https://github.com/IQSS/dataverse/issues/2129
*/
public static final String shibIdpAttribute = "Shib-Identity-Provider";
/**
* @todo Make attribute used (i.e. "eppn") configurable:
* https://github.com/IQSS/dataverse/issues/1422
*
* OR *maybe* we can rely on people installing Dataverse to configure shibd
* to always send "eppn" as an attribute, via attribute mappings or what
* have you.
*/
public static final String uniquePersistentIdentifier = "eppn";
public static final String usernameAttribute = "uid";
public static final String displayNameAttribute = "cn";
public static final String firstNameAttribute = "givenName";
public static final String lastNameAttribute = "sn";
public static final String emailAttribute = "mail";
public static final String testShibIdpEntityId = "https://idp.testshib.org/idp/shibboleth";
/**
* Used to display "Harvard University", for example, based on
* https://dataverse.harvard.edu/Shibboleth.sso/DiscoFeed
*/
public static String getDisplayNameFromDiscoFeed(String entityIdToFind, String discoFeed) {
JsonParser jsonParser = new JsonParser();
JsonElement root = jsonParser.parse(discoFeed);
JsonArray identityProviders = root.getAsJsonArray();
for (JsonElement identityProvider : identityProviders) {
JsonObject provider = identityProvider.getAsJsonObject();
JsonElement entityId = provider.get("entityID");
if (entityId != null) {
if (entityId.getAsString().equals(entityIdToFind)) {
JsonElement displayNamesElement = provider.get("DisplayNames");
if (displayNamesElement != null) {
JsonArray displayNamesArray = displayNamesElement.getAsJsonArray();
JsonElement firstDisplayName = displayNamesArray.get(0);
if (firstDisplayName != null) {
JsonObject friendlyNameObject = firstDisplayName.getAsJsonObject();
if (friendlyNameObject != null) {
JsonElement friendlyNameElement = friendlyNameObject.get("value");
if (friendlyNameElement != null) {
String friendlyName = friendlyNameElement.getAsString();
return friendlyName;
}
}
}
}
}
}
}
return null;
}
/**
* @param displayName Not (yet) used. See @todo.
*
* @todo Do something with displayName. By comparing displayName to the
* firstName and lastName strings, we should be able to figure out where the
* proper split is, like this:
*
* - "Guido|van Rossum"
*
* - "Philip Seymour|Hoffman"
*
* We're not sure how many Identity Providers (IdP) will send us
* "displayName" so we'll hold off on implementing anything for now.
*/
public static ShibUserNameFields findBestFirstAndLastName(String firstName, String lastName, String displayName) {
firstName = findSingleValue(firstName);
lastName = findSingleValue(lastName);
return new ShibUserNameFields(firstName, lastName);
}
public static String findSingleValue(String mayHaveMultipleValues) {
if (mayHaveMultipleValues == null) {
return null;
}
String singleValue = mayHaveMultipleValues;
String[] parts = mayHaveMultipleValues.split(";");
if (parts.length != 1) {
logger.fine("parts (before sorting): " + Arrays.asList(parts));
// predictable order (sorted alphabetically)
Arrays.sort(parts);
logger.fine("parts (after sorting): " + Arrays.asList(parts));
try {
String first = parts[0];
singleValue = first;
} catch (ArrayIndexOutOfBoundsException ex) {
/**
* @todo Is it possible to reach this line via a test? If not,
* remove this try/catch.
*/
logger.fine("Couldn't find first part of " + singleValue);
}
}
return singleValue;
}
public static String generateFriendlyLookingUserIdentifer(String usernameAssertion, String email) {
if (usernameAssertion != null && !usernameAssertion.isEmpty()) {
return usernameAssertion;
}
if (email != null && !email.isEmpty()) {
if (email.contains("@")) {
String[] parts = email.split("@");
try {
String firstPart = parts[0];
return firstPart;
} catch (ArrayIndexOutOfBoundsException ex) {
/**
* @todo Is it possible to reach this line via a test? If
* not, remove this try/catch.
*/
logger.fine(ex + " parsing " + email);
}
} else {
boolean passedValidation = EMailValidator.isEmailValid(email, null);
logger.fine("Odd email address. No @ sign ('" + email + "'). Passed email validation: " + passedValidation);
}
} else {
logger.fine("email attribute not sent by IdP");
}
logger.fine("the best we can do is generate a random UUID");
return UUID.randomUUID().toString();
}
static void mutateRequestForDevConstantTestShib1(HttpServletRequest request) {
request.setAttribute(ShibUtil.shibIdpAttribute, ShibUtil.testShibIdpEntityId);
// the TestShib "eppn" looks like an email address
request.setAttribute(ShibUtil.uniquePersistentIdentifier, "saml@testshib.org");
// request.setAttribute(displayNameAttribute, "Sam El");
request.setAttribute(ShibUtil.firstNameAttribute, "Samuel;Sam");
request.setAttribute(ShibUtil.lastNameAttribute, "El");
// TestShib doesn't send "mail" attribute so let's mimic that.
// request.setAttribute(emailAttribute, "saml@mailinator.com");
request.setAttribute(ShibUtil.usernameAttribute, "saml");
}
static void mutateRequestForDevConstantHarvard1(HttpServletRequest request) {
/**
* Harvard's IdP doesn't send a username (uid).
*/
request.setAttribute(ShibUtil.shibIdpAttribute, "https://fed.huit.harvard.edu/idp/shibboleth");
request.setAttribute(ShibUtil.uniquePersistentIdentifier, "constantHarvard");
/**
* @todo Does Harvard really send displayName? At one point they didn't.
* Let's simulate the non-sending of displayName here.
*/
// request.setAttribute(displayNameAttribute, "John Harvard");
request.setAttribute(ShibUtil.firstNameAttribute, "John");
request.setAttribute(ShibUtil.lastNameAttribute, "Harvard");
request.setAttribute(ShibUtil.emailAttribute, "jharvard@mailinator.com");
request.setAttribute(ShibUtil.usernameAttribute, "jharvard");
}
static void mutateRequestForDevConstantHarvard2(HttpServletRequest request) {
request.setAttribute(ShibUtil.shibIdpAttribute, "https://fed.huit.harvard.edu/idp/shibboleth");
request.setAttribute(ShibUtil.uniquePersistentIdentifier, "constantHarvard2");
// request.setAttribute(displayNameAttribute, "Grace Hopper");
request.setAttribute(ShibUtil.firstNameAttribute, "Grace");
request.setAttribute(ShibUtil.lastNameAttribute, "Hopper");
request.setAttribute(ShibUtil.emailAttribute, "ghopper@mailinator.com");
request.setAttribute(ShibUtil.usernameAttribute, "ghopper");
}
static void mutateRequestForDevConstantTwoEmails(HttpServletRequest request) {
request.setAttribute(ShibUtil.shibIdpAttribute, "https://fake.example.com/idp/shibboleth");
request.setAttribute(ShibUtil.uniquePersistentIdentifier, "twoEmails");
request.setAttribute(ShibUtil.firstNameAttribute, "Eric");
request.setAttribute(ShibUtil.lastNameAttribute, "Allman");
request.setAttribute(ShibUtil.emailAttribute, "eric1@mailinator.com;eric2@mailinator.com");
request.setAttribute(ShibUtil.usernameAttribute, "eallman");
}
static void mutateRequestForDevConstantInvalidEmail(HttpServletRequest request) {
request.setAttribute(ShibUtil.shibIdpAttribute, "https://fake.example.com/idp/shibboleth");
request.setAttribute(ShibUtil.uniquePersistentIdentifier, "invalidEmail");
request.setAttribute(ShibUtil.firstNameAttribute, "Invalid");
request.setAttribute(ShibUtil.lastNameAttribute, "Email");
request.setAttribute(ShibUtil.emailAttribute, "elisah.da mota@example.com");
request.setAttribute(ShibUtil.usernameAttribute, "invalidEmail");
}
static void mutateRequestForDevConstantEmailWithLeadingSpace(HttpServletRequest request) {
request.setAttribute(ShibUtil.shibIdpAttribute, "https://fake.example.com/idp/shibboleth");
request.setAttribute(ShibUtil.uniquePersistentIdentifier, "leadingWhitespace");
request.setAttribute(ShibUtil.firstNameAttribute, "leadingWhitespace");
request.setAttribute(ShibUtil.lastNameAttribute, "leadingWhitespace");
request.setAttribute(ShibUtil.emailAttribute, " leadingWhitespace@mailinator.com");
request.setAttribute(ShibUtil.usernameAttribute, "leadingWhitespace");
}
static void mutateRequestForDevConstantUidWithLeadingSpace(HttpServletRequest request) {
request.setAttribute(ShibUtil.shibIdpAttribute, "https://fake.example.com/idp/shibboleth");
request.setAttribute(ShibUtil.uniquePersistentIdentifier, "leadingWhitespace");
request.setAttribute(ShibUtil.firstNameAttribute, "leadingWhitespace");
request.setAttribute(ShibUtil.lastNameAttribute, "leadingWhitespace");
request.setAttribute(ShibUtil.emailAttribute, "leadingWhitespace@mailinator.com");
request.setAttribute(ShibUtil.usernameAttribute, " leadingWhitespace");
}
// the identifier is the IdP plus the eppn separated by a |
static void mutateRequestForDevConstantIdentifierWithLeadingSpace(HttpServletRequest request) {
request.setAttribute(ShibUtil.shibIdpAttribute, " https://fake.example.com/idp/shibboleth");
request.setAttribute(ShibUtil.uniquePersistentIdentifier, "leadingWhitespace");
request.setAttribute(ShibUtil.firstNameAttribute, "leadingWhitespace");
request.setAttribute(ShibUtil.lastNameAttribute, "leadingWhitespace");
request.setAttribute(ShibUtil.emailAttribute, "leadingWhitespace@mailinator.com");
request.setAttribute(ShibUtil.usernameAttribute, "leadingWhitespace");
}
static void mutateRequestForDevConstantMissingRequiredAttributes(HttpServletRequest request) {
request.setAttribute(ShibUtil.shibIdpAttribute, "https://fake.example.com/idp/shibboleth");
/**
* @todo When shibIdpAttribute is set to null why don't we see the error
* in the GUI?
*/
// request.setAttribute(shibIdpAttribute, null);
request.setAttribute(ShibUtil.uniquePersistentIdentifier, "missing");
request.setAttribute(ShibUtil.uniquePersistentIdentifier, null);
request.setAttribute(ShibUtil.firstNameAttribute, "Missing");
request.setAttribute(ShibUtil.lastNameAttribute, "Required");
request.setAttribute(ShibUtil.emailAttribute, "missing@mailinator.com");
request.setAttribute(ShibUtil.usernameAttribute, "missing");
}
public static Map<String, String> getRandomUserStatic() {
Map<String, String> fakeUser = new HashMap<>();
String shortRandomString = UUID.randomUUID().toString().substring(0, 8);
fakeUser.put("firstName", shortRandomString);
fakeUser.put("lastName", shortRandomString);
fakeUser.put("displayName", shortRandomString + " " + shortRandomString);
fakeUser.put("email", shortRandomString + "@mailinator.com");
fakeUser.put("idp", "https://idp." + shortRandomString + ".com/idp/shibboleth");
fakeUser.put("username", shortRandomString);
fakeUser.put("eppn", shortRandomString);
return fakeUser;
}
/**
* These are attributes that were found to be interesting while developing
* the Shibboleth feature. Only the ones that are defined elsewhere are
* actually used.
*/
static List<String> shibAttrs = Arrays.asList(
ShibUtil.shibIdpAttribute,
ShibUtil.uniquePersistentIdentifier,
ShibUtil.usernameAttribute,
ShibUtil.displayNameAttribute,
ShibUtil.firstNameAttribute,
ShibUtil.lastNameAttribute,
ShibUtil.emailAttribute,
"telephoneNumber",
"affiliation",
"unscoped-affiliation",
"entitlement",
"persistent-id"
);
/**
* These are the attributes we are getting from the IdP at testshib.org, a
* dump from https://pdurbin.pagekite.me/Shibboleth.sso/Session
*
* Miscellaneous
*
* Session Expiration (barring inactivity): 479 minute(s)
*
* Client Address: 10.0.2.2
*
* SSO Protocol: urn:oasis:names:tc:SAML:2.0:protocol
*
* Identity Provider: https://idp.testshib.org/idp/shibboleth
*
* Authentication Time: 2014-09-12T17:07:36.137Z
*
* Authentication Context Class:
* urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
*
* Authentication Context Decl: (none)
*
*
*
* Attributes
*
* affiliation: Member@testshib.org;Staff@testshib.org
*
* cn: Me Myself And I
*
* entitlement: urn:mace:dir:entitlement:common-lib-terms
*
* eppn: myself@testshib.org
*
* givenName: Me Myself
*
* persistent-id:
* https://idp.testshib.org/idp/shibboleth!https://pdurbin.pagekite.me/shibboleth!zylzL+NruovU5OOGXDOL576jxfo=
*
* sn: And I
*
* telephoneNumber: 555-5555
*
* uid: myself
*
* unscoped-affiliation: Member;Staff
*
*/
public static void printAttributes(HttpServletRequest request) {
List<String> shibValues = new ArrayList<>();
if (request == null) {
logger.fine("HttpServletRequest was null. No shib values to print.");
return;
}
for (String attr : shibAttrs) {
/**
* @todo explain in Installers Guide that in order for these
* attributes to be found attributePrefix="AJP_" must be added to
* /etc/shibboleth/shibboleth2.xml like this:
*
* <ApplicationDefaults entityID="https://dataverse.org/shibboleth"
* REMOTE_USER="eppn" attributePrefix="AJP_">
*
*/
Object attrObject = request.getAttribute(attr);
if (attrObject != null) {
shibValues.add(attr + ": " + attrObject.toString());
}
}
logger.fine("shib values: " + shibValues);
}
}