
edu.internet2.middleware.grouper.changeLog.consumer.o365.GraphApiClient Maven / Gradle / Ivy
package edu.internet2.middleware.grouper.changeLog.consumer.o365;
import edu.internet2.middleware.grouper.Group;
import edu.internet2.middleware.grouper.azure.AzureGroupType;
import edu.internet2.middleware.grouper.azure.AzureVisibility;
import edu.internet2.middleware.grouper.azure.model.*;
import edu.internet2.middleware.grouper.changeLog.consumer.Office365ChangeLogConsumer;
import edu.internet2.middleware.grouper.exception.MemberAddAlreadyExistsException;
import edu.internet2.middleware.grouper.exception.MemberDeleteAlreadyDeletedException;
import edu.internet2.middleware.grouper.exception.UnableToPerformException;
import edu.internet2.middleware.grouper.util.GrouperUtil;
import okhttp3.*;
import okhttp3.logging.HttpLoggingInterceptor;
import org.apache.commons.logging.Log;
import retrofit2.Retrofit;
import retrofit2.converter.moshi.MoshiConverterFactory;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.io.IOException;
import java.util.*;
/**
* This class interacts with the Microsoft Graph API.
*/
public class GraphApiClient {
private static final Log logger = GrouperUtil.getLog(GraphApiClient.class);
private final String authUrlBase;
private final String resourceUrlBase;
private final String clientId;
private final String clientSecret;
private final String tenantId;
private final String scope;
private final Office365GraphApiService service;
String token = null;
private final OkHttpClient graphApiHttpClient;
private final OkHttpClient graphTokenHttpClient;
private final AzureGroupType azureGroupType;
private final AzureVisibility visibility;
public GraphApiClient(String authUrlbase, String resourceUrlbase, String clientId, String clientSecret, String tenantId, String scope,
AzureGroupType azureGroupType,
AzureVisibility visibility,
String proxyType, String proxyHost, Integer proxyPort) {
this.authUrlBase = authUrlbase;
this.resourceUrlBase = resourceUrlbase;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.tenantId = tenantId;
this.scope = scope;
this.azureGroupType = azureGroupType;
this.visibility = visibility;
final Proxy proxy;
if (proxyType == null) {
proxy = null; // probably works too: Proxy.NO_PROXY
} else if ("http".equals(proxyType)) {
proxy = new Proxy(Proxy.Type.HTTP,new InetSocketAddress(proxyHost, proxyPort));
} else if ("socks".equals(proxyType)) {
proxy = new Proxy(Proxy.Type.SOCKS,new InetSocketAddress(proxyHost, proxyPort));
} else {
logger.warn("Unable to determine proxy type from '" + proxyType + "'; Valid proxy types for this consumer are 'http' or 'socks'");
proxy = null;
}
graphTokenHttpClient = buildBaseOkHttpClient(proxy);
graphApiHttpClient = buildGraphOkHttpClient(graphTokenHttpClient);
RetrofitWrapper retrofit = buildRetrofit(graphApiHttpClient);
this.service = retrofit.create(Office365GraphApiService.class);
}
protected RetrofitWrapper buildRetrofit(OkHttpClient okHttpClient) {
return new RetrofitWrapper((new Retrofit
.Builder()
.baseUrl(this.resourceUrlBase)
.addConverterFactory(MoshiConverterFactory.create())
.client(okHttpClient)
.build()));
}
protected RetrofitWrapper buildRetrofitAuth(OkHttpClient okHttpClient) {
return new RetrofitWrapper((new Retrofit
.Builder()
.baseUrl(this.authUrlBase + this.tenantId + "/")
.addConverterFactory(MoshiConverterFactory.create())
.client(okHttpClient)
.build()));
}
protected OkHttpClient buildBaseOkHttpClient(Proxy proxy) {
logger.trace("Building OkHttpClient: proxy=" + proxy);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
if (proxy != null) {
builder.proxy(proxy);
}
return builder.build();
}
/*
* customize a shared OkHttpClient instance, which will share the same connection pool, thread pools, and
* configuration as the parent
*/
protected OkHttpClient buildGraphOkHttpClient(OkHttpClient okHttpClient) {
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor((msg) -> {
logger.debug(msg);
});
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
// strips out the Bearer token and replaces with U+2588 (a black square)
loggingInterceptor.redactHeader("Authorization");
return okHttpClient.newBuilder()
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder().header("Authorization", "Bearer " + token).build();
return chain.proceed(request);
}
})
.addInterceptor(loggingInterceptor)
.build();
}
public String getToken() throws IOException {
logger.debug("Token client ID: " + this.clientId);
logger.debug("Token tenant ID: " + this.tenantId);
RetrofitWrapper retrofit = buildRetrofitAuth(this.graphTokenHttpClient);
Office365AuthApiService service = retrofit.create(Office365AuthApiService.class);
retrofit2.Response response = service.getOauth2Token(
"client_credentials",
this.clientId,
this.clientSecret,
this.scope,
"https://graph.microsoft.com")
.execute();
if (response.isSuccessful()) {
AzureGraphOAuthTokenInfo info = response.body();
logTokenInfo(info);
return info.accessToken;
} else {
ResponseBody errorBody = response.errorBody();
throw new IOException("error requesting token (" + response.code() + "): " + errorBody.string());
}
}
private void logTokenInfo(AzureGraphOAuthTokenInfo info) {
logger.trace("Token scope: " + info.scope);
logger.trace("Token expiresIn: " + info.expiresIn);
logger.trace("Token expiresOn: " + info.expiresOn);
logger.trace("Token resource: " + info.resource);
logger.trace("Token tokenType: " + info.tokenType);
logger.trace("Token notBefore: " + info.notBefore);
}
/*
* This method invokes a retrofit API call with retry. If the first call returns 401 (unauthorized)
* the same is retried again after fetching a new token.
*/
private retrofit2.Response invoke(retrofit2.Call call) throws IOException {
for (int retryMax = 2; retryMax > 0; retryMax--) {
if (token == null) {
token = getToken();
}
retrofit2.Response r = call.execute();
if (r.isSuccessful()) {
return r;
} else if (r.code() == 401) {
logger.debug("auth fail, retry: " + call.request().url());
// Call objects cannot be reused, so docs say to use clone() to create a new one with the
// same specs for retry purposes
call = call.clone();
// null out existing token so we'll fetch a new one on next loop pass
token = null;
} else {
throw new IOException("Unhandled invoke response (" + r.code() + ") " + r.errorBody().string());
}
}
throw new IOException("Retry failed for: " + call.request().url());
}
public AzureGraphGroup addGroup(String displayName, String mailNickname, String description) {
logger.debug("Creating group " + displayName + ", group type: " + this.azureGroupType.name());
boolean securityEnabled;
Collection groupTypes = new ArrayList<>();
switch (this.azureGroupType) {
case Security:
securityEnabled = true;
break;
case Unified:
groupTypes.add("Unified");
securityEnabled = false;
break;
case MailEnabled:
case MailEnabledSecurity:
throw new UnableToPerformException("Mail enabled Azure groups are currently not supported");
default:
throw new IllegalStateException("Unexpected value: " + this.azureGroupType);
}
try {
AzureGraphGroup azureGroup = invoke(this.service.createGroup(
new AzureGraphGroup(
null,
displayName,
false,
mailNickname,
securityEnabled,
groupTypes,
description,
visibility
)
)).body();
logger.debug("Created group in Azure: id = " + (azureGroup == null ? "null" : azureGroup.id));
return azureGroup;
} catch (IOException e) {
logger.error(e);
throw new RuntimeException("service.createGroup failed", e);
}
}
public void removeGroup(String groupId) {
try {
invoke(this.service.deleteGroup(groupId));
} catch (IOException e) {
logger.error(e);
throw new RuntimeException("service.deleteGroup failed", e);
}
}
/* In Kansas State's version, this was used to look up users from multiple domains */
public AzureGraphUser lookupMSUser(String userPrincipalName) {
AzureGraphUser user = null;
logger.debug("calling getUserFrom Office365ApiClient");
try {
user = invoke(this.service.getUserByUPN(userPrincipalName)).body();
logger.debug("user = " + (user == null ? "null" : user.toString()));
return user;
} catch (IOException e) {
logger.debug("user principal " + userPrincipalName + " was not found");
}
return null;
}
protected String lookupOffice365GroupId(Group group) {
return group.getAttributeValueDelegate().retrieveValueString(Office365ChangeLogConsumer.GROUP_ID_ATTRIBUTE_NAME);
}
public void addMemberToMS(String groupId, String userPrincipalName) {
try {
invoke(this.service.addGroupMember(groupId, new AzureGraphDataIdContainer(resourceUrlBase + "users/" + userPrincipalName)));
} catch (IOException e) {
logger.error(e.getMessage(), e);
} catch (MemberAddAlreadyExistsException me) {
logger.debug("member already exists for subject:" + userPrincipalName + " and group:" + groupId);
}
}
public void removeMembership(String userPrincipalName, Group group) {
try {
if (group != null) {
AzureGraphUser user = lookupMSUser(userPrincipalName);
if (user == null) {
throw new RuntimeException("Failed to locate member: " + userPrincipalName);
}
String groupId = lookupOffice365GroupId(group);
if (ifUserAndGroupExistInMS(user, groupId)) {
removeUserFromGroupInMS(user.id, groupId);
}
}
} catch (IOException e) {
logger.error(e);
} catch (MemberDeleteAlreadyDeletedException me) {
logger.debug("member already deleted for subject:" + userPrincipalName + " and group:" + group.getId());
}
}
protected boolean ifUserAndGroupExistInMS(AzureGraphUser user, String groupId) {
return user != null && groupId != null;
}
public void removeUserFromGroupInMS(String groupId, String userId) throws IOException {
invoke(this.service.removeGroupMember(groupId, userId));
}
public List getGroups() throws IOException {
AzureGraphGroups groupContainer = invoke(this.service.getGroups(Collections.emptyMap())).body();
return groupContainer.groups;
}
public List getGroupMembers(String groupId) throws IOException {
AzureGraphGroupMembers azureGroupMembers = invoke(this.service.getGroupMembers(groupId)).body();
return azureGroupMembers.users;
}
public AzureGraphGroup retrieveGroup(String groupId) throws IOException {
return invoke(this.service.getGroup(groupId)).body();
}
public List getAllUsers() throws IOException {
AzureGraphUsers azureGraphUsers = invoke(this.service.getUsers()).body();
return azureGraphUsers.users;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy