package com.constellio.model.conf.ldap.services;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Transformer;
import org.apache.commons.lang3.StringUtils;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.JerseyClientBuilder;
import org.joda.time.DateTime;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.constellio.model.conf.ldap.config.LDAPServerConfiguration;
import com.constellio.model.conf.ldap.config.LDAPUserSyncConfiguration;
import com.constellio.model.conf.ldap.services.LDAPServicesException.CouldNotConnectUserToLDAP;
import com.constellio.model.conf.ldap.user.LDAPGroup;
import com.constellio.model.conf.ldap.user.LDAPUser;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.microsoft.aad.adal4j.AuthenticationContext;
import com.microsoft.aad.adal4j.AuthenticationResult;
import com.microsoft.aad.adal4j.ClientCredential;
/**
*/
public class AzureAdClient {
public static class AzureAdClientException extends RuntimeException {
public AzureAdClientException(String message) {
super(message);
}
public AzureAdClientException(String message, Throwable cause) {
super(message, cause);
}
}
static class RequestHelper {
@VisibleForTesting
static int maxResults = 150;
private static String getResponseText(final Response response) {
return response.readEntity(String.class).replace("\uFEFF", "");
}
private static String getSkipToken(final String responseText) {
if (responseText.contains("odata.nextLink")) {
for (String string : new JSONObject(responseText).getString("odata.nextLink").split("\\?")) {
for (String stringOfString : string.split("&")) {
if (stringOfString.startsWith("$skiptoken")) {
return stringOfString.split("=")[1];
}
}
}
return null;
} else {
return null;
}
}
private String tenantName;
private String clientId;
private String clientSecret;
private Client client;
private WebTarget webTarget;
private AuthenticationResult authenticationResult;
public RequestHelper(final String tenantName, final String clientId, final String clientSecret) {
this.tenantName = tenantName;
this.clientId = clientId;
this.clientSecret = clientSecret;
client = JerseyClientBuilder.newClient();
client.property(ClientProperties.CONNECT_TIMEOUT, 300000);
client.property(ClientProperties.READ_TIMEOUT, 300000);
webTarget = client.target(GRAPH_API_URL + tenantName);
}
private void acquireAccessToken() {
acquireAccessToken(authenticationResult == null || authenticationResult.getAccessToken() == null);
}
private void acquireAccessToken(boolean ignoreCurrent) {
if (ignoreCurrent) {
String authority = AUTHORITY_BASE_URL + tenantName;
ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
AuthenticationContext authenticationContext = new AuthenticationContext(authority, true, executorService);
Future<AuthenticationResult> authenticationResultFuture = authenticationContext.acquireToken(
GRAPH_API_URL,
new ClientCredential(clientId, clientSecret),
null
);
authenticationResult = authenticationResultFuture.get();
} catch (MalformedURLException mue) {
throw new AzureAdClientException("Malformed Azure AD authority URL " + authority);
} catch (ExecutionException ee) {
throw new AzureAdClientException(
"Can't acquire an Azure AD token for client " + clientId + " with the provided secret key", ee);
} catch (InterruptedException ignored) {
} finally {
executorService.shutdown();
}
}
}
private void refreshAccessToken() {
if (authenticationResult == null || authenticationResult.getRefreshToken() == null) {
acquireAccessToken(true);
} else {
String authority = AUTHORITY_BASE_URL + tenantName;
ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
AuthenticationContext authenticationContext = new AuthenticationContext(authority, true, executorService);
Future<AuthenticationResult> authenticationResultFuture = authenticationContext.acquireTokenByRefreshToken(
authenticationResult.getRefreshToken(),
new ClientCredential(clientId, clientSecret),
null
);
authenticationResult = authenticationResultFuture.get();
} catch (MalformedURLException mue) {
throw new AzureAdClientException("Malformed Azure AD authority URL " + authority);
} catch (ExecutionException ee) {
throw new AzureAdClientException(
"Can't acquire an Azure AD token for client " + clientId + " with the provided secret key", ee);
} catch (InterruptedException ignored) {
} finally {
executorService.shutdown();
}
}
}
private Invocation.Builder completeQueryBuilding(WebTarget webTarget, String filter) {
WebTarget newWebTarget = webTarget.queryParam("api-version", GRAPH_API_VERSION);
if (StringUtils.isNotEmpty(filter)) {
return newWebTarget
.queryParam("$filter", filter)
.request(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, authenticationResult.getAccessToken());
}
return newWebTarget
.request(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, authenticationResult.getAccessToken());
}
private Invocation.Builder completeQueryBuilding(WebTarget webTarget, String filter, String skipToken) {
WebTarget newWebTarget = webTarget.queryParam("api-version", GRAPH_API_VERSION).queryParam("$top", maxResults);
if (StringUtils.isNotEmpty(filter)) {
newWebTarget = newWebTarget.queryParam("$filter", filter);
}
if (skipToken == null) {
return newWebTarget
.request(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, authenticationResult.getAccessToken());
}
return newWebTarget
.queryParam("$skiptoken", skipToken)
.request(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, authenticationResult.getAccessToken());
}
private JSONArray submitQueryWithoutPagination(WebTarget webTarget, String filter) {
String responseText;
acquireAccessToken();
Response response = completeQueryBuilding(webTarget, filter).get();
if (response.getStatus() == HttpURLConnection.HTTP_UNAUTHORIZED) {
refreshAccessToken();
response = completeQueryBuilding(webTarget, filter).get();
}
if (new Integer(response.getStatus()).toString().startsWith("5")) {
try {
Thread.sleep(5000);
} catch (InterruptedException ignored) {
}
response = completeQueryBuilding(webTarget, filter).get();
}
responseText = getResponseText(response);
if (response.getStatus() == HttpURLConnection.HTTP_OK) {
return new JSONObject(responseText).getJSONArray("value");
} else if (new Integer(response.getStatus()).toString().startsWith("5")) {
LOGGER.error(responseText);
throw new AzureAdClientException("Unexpected Azure AD Graph API server error");
} else {
throw new AzureAdClientException(
new JSONObject(responseText).optJSONObject("odata.error").optJSONObject("message").optString("value"));
}
}
private List<JSONArray> submitQueryWithPagination(WebTarget webTarget, String filter) {
List<JSONArray> result = new ArrayList<>();
String responseText;
String skipToken = null;
do {
acquireAccessToken();
Response response = completeQueryBuilding(webTarget, filter, skipToken).get();
if (response.getStatus() == HttpURLConnection.HTTP_UNAUTHORIZED) {
refreshAccessToken();
response = completeQueryBuilding(webTarget, filter, skipToken).get();
}
if (new Integer(response.getStatus()).toString().startsWith("5")) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
break;
}
response = completeQueryBuilding(webTarget, filter, skipToken).get();
}
responseText = getResponseText(response);
skipToken = getSkipToken(responseText);
if (response.getStatus() == HttpURLConnection.HTTP_OK) {
result.add(new JSONObject(responseText).getJSONArray("value"));
} else if (new Integer(response.getStatus()).toString().startsWith("5")) {
LOGGER.error(responseText);
throw new AzureAdClientException("Unexpected Azure AD Graph API server error");
} else {
throw new AzureAdClientException(
new JSONObject(responseText).optJSONObject("odata.error").optJSONObject("message")
.optString("value"));
}
} while (skipToken != null);
return result;
}
@VisibleForTesting
List<JSONArray> getAllUsersResponse(List<String> userGroups, String usersFilter) {
List<JSONArray> jsonArrayList;
if (CollectionUtils.isNotEmpty(userGroups)) {
jsonArrayList = new ArrayList<>();
for (JSONArray jsonArray : getAllGroupsResponse(buildUserGroupsFilter(userGroups))) {
for (int i = 0, length = jsonArray.length(); i < length; i++) {
String objectId = jsonArray.getJSONObject(i).optString("objectId");
jsonArrayList.addAll(getGroupMembersResponse(objectId));
}
}
} else {
jsonArrayList = getAllUsersResponse(usersFilter);
}
return jsonArrayList;
}
private String buildUserGroupsFilter(List<String> userGroups) {
return Joiner.on(" and ").join(CollectionUtils.collect(userGroups, new Transformer() {
@Override
public Object transform(Object input) {
return "(displayName eq '" + input + "')";
}
}));
}
List<JSONArray> getAllUsersResponse(String usersFilter) {
return submitQueryWithPagination(webTarget.path("users"), usersFilter);
}
@VisibleForTesting
List<JSONArray> getUserGroupsResponse(final String userObjectId) {
return submitQueryWithPagination(webTarget.path("users").path(userObjectId).path("$links").path("memberOf"), null);
}
@VisibleForTesting
List<JSONArray> getAllGroupsResponse(String groupsFilter) {
return submitQueryWithPagination(webTarget.path("groups"), groupsFilter);
}
@VisibleForTesting
List<JSONArray> getGroupMembersResponse(final String groupObjectId) {
return submitQueryWithPagination(webTarget.path("groups").path(groupObjectId).path("$links").path("members"), null);
}
private JSONObject getObjectResponseByUrl(final String objectUrl) {
String responseText;
acquireAccessToken();
Response response = getObjectByUrl(objectUrl);
if (response.getStatus() == HttpURLConnection.HTTP_UNAUTHORIZED) {
refreshAccessToken();
response = getObjectByUrl(objectUrl);
}
if (new Integer(response.getStatus()).toString().startsWith("5")) {
try {
Thread.sleep(5000);
} catch (InterruptedException ignored) {
}
response = getObjectByUrl(objectUrl);
}
responseText = getResponseText(response);
if (response.getStatus() == HttpURLConnection.HTTP_OK) {
return new JSONObject(responseText);
} else if (new Integer(response.getStatus()).toString().startsWith("5")) {
LOGGER.error(responseText);
throw new AzureAdClientException("Unexpected Azure AD Graph API server error");
} else {
throw new AzureAdClientException(
new JSONObject(responseText).optJSONObject("odata.error").optJSONObject("message").optString("value"));
}
}
private Response getObjectByUrl(final String objectUrl) {
return completeQueryBuilding(client.target(objectUrl), null).get();
}
}
private static final Logger LOGGER = LoggerFactory.getLogger(AzureAdClient.class);
// TODO : Use "graph.microsoft.com/v1.0" instead as recommended in https://blogs.msdn.microsoft.com/aadgraphteam/2016/07/08/microsoft-graph-or-azure-ad-graph/
private static final String GRAPH_API_URL = "https://graph.windows.net/";
private static final String GRAPH_API_VERSION = "1.6";
private static final String AUTHORITY_BASE_URL = "https://login.microsoftonline.com/";
private LDAPServerConfiguration ldapServerConfiguration;
private LDAPUserSyncConfiguration ldapUserSyncConfiguration;
public AzureAdClient(final LDAPServerConfiguration ldapServerConfiguration,
final LDAPUserSyncConfiguration ldapUserSyncConfiguration) {
this.ldapServerConfiguration = ldapServerConfiguration;
this.ldapUserSyncConfiguration = ldapUserSyncConfiguration;
}
public Set<String> getUserNameList() {
LOGGER.info("Getting user name list - start");
Set<String> results = new HashSet<>();
RequestHelper requestHelper = new RequestHelper(
ldapServerConfiguration.getTenantName(),
ldapUserSyncConfiguration.getClientId(),
ldapUserSyncConfiguration.getClientSecret());
List<JSONArray> jsonArrayList = requestHelper
.getAllUsersResponse(ldapUserSyncConfiguration.getUserGroups(), ldapUserSyncConfiguration.getUsersFilter());
for (JSONArray jsonArray : jsonArrayList) {
for (int i = 0, length = jsonArray.length(); i < length; i++) {
String userName = jsonArray.getJSONObject(i).optString("userPrincipalName");
if (ldapUserSyncConfiguration.isUserAccepted(userName)) {
results.add(userName);
}
}
}
LOGGER.info("Getting user name list - end");
return results;
}
public Set<String> getGroupNameList() {
LOGGER.info("Getting group name list - start");
Set<String> results = new HashSet<>();
RequestHelper requestHelper = new RequestHelper(
ldapServerConfiguration.getTenantName(),
ldapUserSyncConfiguration.getClientId(),
ldapUserSyncConfiguration.getClientSecret());
for (JSONArray jsonArray : requestHelper.getAllGroupsResponse(ldapUserSyncConfiguration.getGroupsFilter())) {
for (int i = 0, length = jsonArray.length(); i < length; i++) {
String groupName = jsonArray.getJSONObject(i).optString("displayName");
if (ldapUserSyncConfiguration.isGroupAccepted(groupName)) {
results.add(groupName);
}
}
}
LOGGER.info("Getting group name list - end");
return results;
}
public void getGroupsAndTheirUsers(final Map<String, LDAPGroup> ldapGroups, final Map<String, LDAPUser> ldapUsers) {
LOGGER.info("Getting groups and their members - start");
RequestHelper requestHelper = new RequestHelper(
ldapServerConfiguration.getTenantName(),
ldapUserSyncConfiguration.getClientId(),
ldapUserSyncConfiguration.getClientSecret());
int pageNum = 1;
List<JSONArray> stuff = requestHelper.getAllGroupsResponse(ldapUserSyncConfiguration.getGroupsFilter());
for (JSONArray groupsArray : stuff) {
int groupsPageSize = groupsArray.length();
LOGGER.info("Processing groups page " + pageNum++ + " having " + groupsPageSize + " items");
for (int ig = 0; ig < groupsPageSize; ig++) {
LDAPGroup ldapGroup = buildLDAPGroupFromJsonObject(groupsArray.getJSONObject(ig));
if (ldapUserSyncConfiguration.isGroupAccepted(ldapGroup.getSimpleName())) {
if (ldapGroups.containsKey(ldapGroup.getDistinguishedName())) {
ldapGroup = ldapGroups.get(ldapGroup.getDistinguishedName());
} else {
ldapGroups.put(ldapGroup.getDistinguishedName(), ldapGroup);
}
for (JSONArray membersArray : requestHelper.getGroupMembersResponse(ldapGroup.getDistinguishedName())) {
for (int im = 0, membersCount = membersArray.length(); im < membersCount; im++) {
String objectUrl = membersArray.getJSONObject(im).optString("url");
if (objectUrl.endsWith("Microsoft.DirectoryServices.User")) {
JSONObject jsonObject = requestHelper.getObjectResponseByUrl(objectUrl);
LDAPUser ldapUser = buildLDAPUserFromJsonObject(jsonObject);
//if (ldapUserSyncConfiguration.isUserAccepted(ldapUser.getName())) {
if (ldapUsers.containsKey(ldapUser.getId())) {
ldapUser = ldapUsers.get(ldapUser.getId());
} else {
ldapUsers.put(ldapUser.getId(), ldapUser);
}
ldapGroup.addUser(ldapUser.getId());
ldapUser.addGroup(ldapGroup);
//}
}
}
}
}
}
}
LOGGER.info("Getting groups and their members - end");
}
private LDAPGroup buildLDAPGroupFromJsonObject(JSONObject groupJsonObject) {
String groupObjectId = groupJsonObject.optString("objectId");
String groupDisplayName = groupJsonObject.optString("displayName");
return new LDAPGroup(groupDisplayName, groupObjectId);
}
private LDAPUser buildLDAPUserFromJsonObject(JSONObject userJsonObject) {
LDAPUser ldapUser = new LDAPUser();
ldapUser.setId(userJsonObject.optString("objectId"));
ldapUser.setName(userJsonObject.optString("mailNickname"));//not displayName
ldapUser.setFamilyName(userJsonObject.optString("surname"));
ldapUser.setGivenName(userJsonObject.optString("givenName"));
//ldapUser.setEmail(userJsonObject.optString("email")); there mail but with several values instead we ll use userPrincipalName
ldapUser.setEmail(userJsonObject.optString("userPrincipalName"));
ldapUser.setEnabled(Boolean.valueOf(userJsonObject.optString("accountEnabled")));
ldapUser.setLieuTravail(userJsonObject.optString("department"));
ldapUser.setMsExchDelegateListBL(null); // TODO
String refreshTokensValidFromDateTime = userJsonObject.optString("refreshTokensValidFromDateTime");
if (!StringUtils.isEmpty(refreshTokensValidFromDateTime) && !"null".equals(refreshTokensValidFromDateTime)) {
ldapUser.setLastLogon(new DateTime(refreshTokensValidFromDateTime).toDate());
}
return ldapUser;
}
public void getUsersAndTheirGroups(final Map<String, LDAPGroup> ldapGroups, final Map<String, LDAPUser> ldapUsers) {
LOGGER.info("Getting users and their memberships - start");
RequestHelper requestHelper = new RequestHelper(
ldapServerConfiguration.getTenantName(),
ldapUserSyncConfiguration.getClientId(),
ldapUserSyncConfiguration.getClientSecret());
int pageNum = 1;
List<JSONArray> jsonArrayList = requestHelper
.getAllUsersResponse(ldapUserSyncConfiguration.getUserGroups(), ldapUserSyncConfiguration.getUsersFilter());
for (JSONArray userArray : jsonArrayList) {
int groupsPageSize = userArray.length();
LOGGER.info("Processing users page " + pageNum++ + " having " + groupsPageSize + " items");
for (int iu = 0; iu < groupsPageSize; iu++) {
JSONObject jsonObject = userArray.getJSONObject(iu);
if (jsonObject.has("url")) {
jsonObject = requestHelper.getObjectResponseByUrl(jsonObject.optString("url"));
}
LDAPUser ldapUser = buildLDAPUserFromJsonObject(jsonObject);
if (ldapUserSyncConfiguration.isUserAccepted(ldapUser.getName())) {
if (ldapUsers.containsKey(ldapUser.getId())) {
ldapUser = ldapUsers.get(ldapUser.getId());
} else {
ldapUsers.put(ldapUser.getId(), ldapUser);
}
//}
for (JSONArray membershipsArray : requestHelper.getUserGroupsResponse(ldapUser.getId())) {
for (int im = 0, membershipsCount = membershipsArray.length(); im < membershipsCount; im++) {
String objectUrl = membershipsArray.getJSONObject(im).optString("url");
if (objectUrl.endsWith("Microsoft.DirectoryServices.Group")) {
jsonObject = requestHelper.getObjectResponseByUrl(objectUrl);
LDAPGroup ldapGroup = buildLDAPGroupFromJsonObject(jsonObject);
//if (ldapUserSyncConfiguration.isGroupAccepted(ldapGroup.getSimpleName())) {
if (ldapGroups.containsKey(ldapGroup.getDistinguishedName())) {
ldapGroup = ldapGroups.get(ldapGroup.getDistinguishedName());
} else {
ldapGroups.put(ldapGroup.getDistinguishedName(), ldapGroup);
}
ldapGroup.addUser(ldapUser.getId());
ldapUser.addGroup(ldapGroup);
//}
}
}
}
}
}
}
LOGGER.info("Getting users and their memberships - end");
}
public void authenticate(final String user, final String password)
throws CouldNotConnectUserToLDAP {
String authority = AUTHORITY_BASE_URL + ldapServerConfiguration.getTenantName();
ExecutorService executorService = Executors.newSingleThreadExecutor();
AuthenticationResult authenticationResult = null;
try {
AuthenticationContext authenticationContext = new AuthenticationContext(authority, true, executorService);
Future<AuthenticationResult> authenticationResultFuture = authenticationContext.acquireToken(
GRAPH_API_URL,
ldapServerConfiguration.getClientId(),
user,
password,
null);
authenticationResult = authenticationResultFuture.get();
} catch (MalformedURLException mue) {
LOGGER.error("Malformed Azure AD authority URL " + authority);
throw new CouldNotConnectUserToLDAP();
} catch (ExecutionException ee) {
LOGGER.error("Can't authenticate user " + user);
throw new CouldNotConnectUserToLDAP();
} catch (InterruptedException ignored) {
} finally {
executorService.shutdown();
}
if (authenticationResult == null) {
throw new CouldNotConnectUserToLDAP();
}
}
}