/**
* Copyright 2014 Microsoft Open Technologies Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoftopentechnologies.intellij.helpers.o365;
import com.google.common.base.*;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.*;
import com.google.gson.Gson;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.project.Project;
import com.microsoft.directoryservices.*;
import com.microsoft.directoryservices.odata.ApplicationFetcher;
import com.microsoft.directoryservices.odata.DirectoryClient;
import com.microsoft.directoryservices.odata.DirectoryObjectOperations;
import com.microsoft.services.odata.ODataCollectionFetcher;
import com.microsoft.services.odata.ODataEntityFetcher;
import com.microsoft.services.odata.ODataOperations;
import com.microsoftopentechnologies.intellij.components.MSOpenTechToolsApplication;
import com.microsoftopentechnologies.intellij.components.PluginSettings;
import com.microsoftopentechnologies.intellij.helpers.StringHelper;
import com.microsoftopentechnologies.intellij.helpers.aadauth.AuthenticationContext;
import com.microsoftopentechnologies.intellij.helpers.aadauth.AuthenticationResult;
import com.microsoftopentechnologies.intellij.helpers.aadauth.PromptValue;
import com.microsoftopentechnologies.intellij.helpers.graph.PluginDependencyResolver;
import com.microsoftopentechnologies.intellij.helpers.graph.ServicePermissionEntry;
import com.microsoftopentechnologies.intellij.model.Office365Permission;
import com.microsoftopentechnologies.intellij.model.Office365PermissionList;
import com.microsoftopentechnologies.intellij.model.Office365Service;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.ReentrantLock;
public class Office365RestAPIManager implements Office365Manager {
public static final String GRAPH_API_URI_TEMPLATE = "{base_uri}{tenant_domain}?api-version={api_version}";
public static final String PROJECT_APP_ID = "com.microsoftopentechnologies.intellij.ProjectAppId";
private static Office365Manager instance;
private AuthenticationResult authenticationToken;
private ReentrantLock authenticationTokenLock = new ReentrantLock();
private String tenantDomain;
private ReentrantLock tenantDomainLock = new ReentrantLock();
private String graphApiUri;
private ReentrantLock graphApiUriLock = new ReentrantLock();
private ReentrantLock refreshTokenLock = new ReentrantLock();
private DirectoryClient directoryDataServiceClient;
private ReentrantLock directoryDataServiceClientLock = new ReentrantLock();
public class ServiceAppIds {
public static final String EXCHANGE = "00000002-0000-0ff1-ce00-000000000000";
public static final String SHARE_POINT = "00000003-0000-0ff1-ce00-000000000000";
public static final String AZURE_ACTIVE_DIRECTORY = "00000002-0000-0000-c000-000000000000";
}
private Office365RestAPIManager() {
}
@NotNull
public static Office365Manager getManager() {
if (instance == null) {
instance = new Office365RestAPIManager();
}
return instance;
}
@Override
public boolean authenticated() throws ParseException {
return getAuthenticationToken() != null;
}
private void resetState() {
setTenantDomain(null);
setGraphApiUri(null);
setDirectoryDataServiceClient(null);
}
@Override
public void setAuthenticationToken(AuthenticationResult token) {
// if the authentication token is changing then pretty
// much all other state needs to be reset
resetState();
if (token != null) {
Gson gson = new Gson();
String json = gson.toJson(token, AuthenticationResult.class);
PropertiesComponent.getInstance().setValue(MSOpenTechToolsApplication.AppSettingsNames.O365_AUTHENTICATION_TOKEN, json);
} else {
PropertiesComponent.getInstance().unsetValue(MSOpenTechToolsApplication.AppSettingsNames.O365_AUTHENTICATION_TOKEN);
}
// reference assignments in java are atomic; so we don't need a
// try/finally block here to ensure that the lock isn't left locked
authenticationTokenLock.lock();
authenticationToken = token;
authenticationTokenLock.unlock();
}
@Override
public AuthenticationResult getAuthenticationToken() {
if (authenticationToken == null) {
String json = PropertiesComponent.getInstance().getValue(MSOpenTechToolsApplication.AppSettingsNames.O365_AUTHENTICATION_TOKEN);
if (!StringHelper.isNullOrWhiteSpace(json)) {
Gson gson = new Gson();
setAuthenticationToken(gson.fromJson(json, AuthenticationResult.class));
}
}
return authenticationToken;
}
@Override
public void authenticate() throws IOException, ExecutionException, InterruptedException, ParseException {
PluginSettings settings = MSOpenTechToolsApplication.getCurrent().getSettings();
AuthenticationContext context = null;
try {
context = new AuthenticationContext(settings.getAdAuthority());
ListenableFuture<AuthenticationResult> future = context.acquireTokenInteractiveAsync(
authenticated() ? getTenantDomain() : settings.getTenantName(),
settings.getGraphApiUri(),
settings.getClientId(),
settings.getRedirectUri(),
null,
PromptValue.login);
setAuthenticationToken(future.get());
} finally {
if (context != null) {
context.dispose();
}
}
}
private String getGraphApiUri() throws ParseException {
if (graphApiUri == null) {
PluginSettings settings = MSOpenTechToolsApplication.getCurrent().getSettings();
setGraphApiUri(GRAPH_API_URI_TEMPLATE.
replace("{base_uri}", settings.getGraphApiUri()).
replace("{tenant_domain}", getTenantDomain()).
replace("{api_version}", settings.getGraphApiVersion()));
}
return graphApiUri;
}
private void setGraphApiUri(String graphApiUri) {
graphApiUriLock.lock();
this.graphApiUri = graphApiUri;
graphApiUriLock.unlock();
}
// NOTE: The result of calling getDirectoryClient should never be cached. This is because of the following
// reasons:
// [a] every directory client object is associated with an authentication token
// [b] as part of execution of the method, tokens might expire and be renewed in which case a new directory
// client will be instantiated; if we use cached objects then we'll continue using the client with the
// expired token instead of the new one
private DirectoryClient getDirectoryClient() throws ParseException {
if (directoryDataServiceClient == null) {
PluginDependencyResolver dependencyResolver = new PluginDependencyResolver(getAuthenticationToken().getAccessToken());
setDirectoryDataServiceClient(new DirectoryClient(getGraphApiUri(), dependencyResolver));
}
return directoryDataServiceClient;
}
private void setDirectoryDataServiceClient(DirectoryClient directoryDataServiceClient) {
directoryDataServiceClientLock.lock();
this.directoryDataServiceClient = directoryDataServiceClient;
directoryDataServiceClientLock.unlock();
}
private <T> void requestWithInteractiveToken(
RequestCallback<T> requestCallback,
final SettableFuture<T> wrappedFuture) throws ParseException {
// do interactive auth
try {
authenticate();
} catch (IOException e) {
wrappedFuture.setException(e);
return;
} catch (ExecutionException e) {
wrappedFuture.setException(e);
return;
} catch (InterruptedException e) {
wrappedFuture.setException(e);
return;
} catch (ParseException e) {
wrappedFuture.setException(e);
return;
}
Futures.addCallback(requestCallback.execute(), new FutureCallback<T>() {
@Override
public void onSuccess(T val) {
wrappedFuture.set(val);
}
@Override
public void onFailure(Throwable throwable) {
wrappedFuture.setException(throwable);
}
});
}
private <T> void requestWithRefreshToken(
final RequestCallback<T> requestCallback,
final SettableFuture<T> wrappedFuture) throws ParseException {
// acquire token via refresh token
PluginSettings settings = MSOpenTechToolsApplication.getCurrent().getSettings();
AuthenticationContext context = null;
// Now there might be multiple concurrent requests all of which are likely
// to end up needing a new access token all at the same time. In this case
// we don't want to issue multiple requests redeeming refresh tokens as that
// is wasteful. To prevent that we serialize the code block below using a
// re-entrant lock. We do this by first acquiring the current authentication
// token before acquiring the lock which may or may not have a value. After the
// lock has been acquired, the following are the possibilities:
// +-------------------+------------------+--------+----------------------+-------------------------+
// | Value before lock | Value after lock | Equal? | Issue Token Request? | Remarks |
// +-------------------+------------------+--------+----------------------+-------------------------+
// | null | null | yes | yes | |
// | null | not null | no | no | |
// | not null | null | no | no | This indicates an error |
// | not null | not null | no | no | |
// | not null | not null | yes | yes | |
// +-------------------+------------------+--------+----------------------+-------------------------+
AuthenticationResult token = getAuthenticationToken();
refreshTokenLock.lock();
try {
if (AuthenticationResult.equals(token, getAuthenticationToken())) {
context = new AuthenticationContext(settings.getAdAuthority());
setAuthenticationToken(context.acquireTokenByRefreshToken(
getAuthenticationToken(),
getTenantDomain(),
settings.getGraphApiUri(),
settings.getClientId()));
}
} catch (IOException e) {
wrappedFuture.setException(e);
return;
} catch (ParseException e) {
wrappedFuture.setException(e);
return;
} finally {
// we release the lock before we do anything else as
// we don't want a lock leaking in case the statements
// following throw
refreshTokenLock.unlock();
if (context != null) {
context.dispose();
}
}
Futures.addCallback(requestCallback.execute(), new FutureCallback<T>() {
@Override
public void onSuccess(T val) {
wrappedFuture.set(val);
}
@Override
public void onFailure(Throwable throwable) {
if (isErrorUnauthorized(throwable)) {
try {
requestWithInteractiveToken(requestCallback, wrappedFuture);
} catch (ParseException e) {
wrappedFuture.setException(throwable);
}
} else {
wrappedFuture.setException(throwable);
}
}
});
}
private <T> ListenableFuture<T> requestWithToken(final RequestCallback<T> requestCallback) throws ParseException {
final SettableFuture<T> wrappedFuture = SettableFuture.create();
Futures.addCallback(requestCallback.execute(), new FutureCallback<T>() {
@Override
public void onSuccess(T val) {
wrappedFuture.set(val);
}
@Override
public void onFailure(Throwable throwable) {
if (isErrorUnauthorized(throwable)) {
try {
requestWithRefreshToken(requestCallback, wrappedFuture);
} catch (ParseException e) {
wrappedFuture.setException(throwable);
}
} else {
wrappedFuture.setException(throwable);
}
}
});
return wrappedFuture;
}
private boolean isErrorUnauthorized(Throwable throwable) {
return throwable.getMessage().contains("Authentication_ExpiredToken");
}
@NotNull
@Override
public ListenableFuture<List<Application>> getApplicationList() throws ParseException {
return requestWithToken(new RequestCallback<List<Application>>() {
@Override
public ListenableFuture<List<Application>> execute() throws ParseException {
return getAllObjects(getDirectoryClient().getapplications());
}
});
}
@Override
public ListenableFuture<Application> getApplicationByObjectId(final String objectId) throws ParseException {
return requestWithToken(new RequestCallback<Application>() {
@Override
public ListenableFuture<Application> execute() throws ParseException {
return getDirectoryClient().getapplications().getById(objectId).read();
}
});
}
@Override
public ListenableFuture<List<ServicePermissionEntry>> getO365PermissionsForApp(final String objectId) throws ParseException {
return requestWithToken(new RequestCallback<List<ServicePermissionEntry>>() {
@Override
public ListenableFuture<List<ServicePermissionEntry>> execute() throws ParseException {
return Futures.transform(getApplicationByObjectId(objectId), new AsyncFunction<Application, List<ServicePermissionEntry>>() {
@Override
public ListenableFuture<List<ServicePermissionEntry>> apply(Application application) throws Exception {
final String[] filterAppIds = new String[]{
ServiceAppIds.SHARE_POINT,
ServiceAppIds.EXCHANGE,
ServiceAppIds.AZURE_ACTIVE_DIRECTORY
};
// build initial list of permission from the app's permissions
final List<ServicePermissionEntry> servicePermissions = getO365PermissionsFromResourceAccess(application.getrequiredResourceAccess(), filterAppIds);
// get permissions list from O365 service principals
return Futures.transform(getServicePrincipalsForO365(), new AsyncFunction<List<ServicePrincipal>, List<ServicePermissionEntry>>() {
@Override
public ListenableFuture<List<ServicePermissionEntry>> apply(List<ServicePrincipal> servicePrincipals) throws Exception {
for (final ServicePrincipal servicePrincipal : servicePrincipals) {
// lookup this service principal in app's list of resources; if it's not found add an entry
ServicePermissionEntry servicePermissionEntry = Iterables.find(servicePermissions, new Predicate<ServicePermissionEntry>() {
@Override
public boolean apply(ServicePermissionEntry servicePermissionEntry) {
return servicePermissionEntry.getKey().getId().equals(servicePrincipal.getappId());
}
}, null);
if (servicePermissionEntry == null) {
servicePermissions.add(servicePermissionEntry = new ServicePermissionEntry(
new Office365Service(),
new Office365PermissionList()
));
}
Office365Service service = servicePermissionEntry.getKey();
Office365PermissionList permissionList = servicePermissionEntry.getValue();
service.setId(servicePrincipal.getappId());
service.setName(servicePrincipal.getdisplayName());
List<OAuth2Permission> permissions = servicePrincipal.getoauth2Permissions();
for (final OAuth2Permission permission : permissions) {
// lookup permission in permissionList
Office365Permission office365Permission = Iterables.find(permissionList, new Predicate<Office365Permission>() {
@Override
public boolean apply(Office365Permission office365Permission) {
return office365Permission.getId().equals(permission.getid().toString());
}
}, null);
if (office365Permission == null) {
permissionList.add(office365Permission = new Office365Permission());
office365Permission.setEnabled(false);
}
office365Permission.setId(permission.getid().toString());
office365Permission.setName(getPermissionDisplayName(permission.getvalue()));
office365Permission.setDescription(permission.getuserConsentDisplayName());
}
}
return Futures.immediateFuture(servicePermissions);
}
});
}
});
}
});
}
private String getPermissionDisplayName(String displayName) {
// replace '.' and '_' with space characters and title case the display name
return Joiner.on(' ').
join(Iterables.transform(
Splitter.on(' ').split(
CharMatcher.anyOf("._").
replaceFrom(displayName, ' ')),
new Function<String, String>() {
@Override
public String apply(String str) {
return Character.toUpperCase(str.charAt(0)) + str.substring(1);
}
}
)
);
}
private List<ServicePermissionEntry> getO365PermissionsFromResourceAccess(
List<RequiredResourceAccess> requiredResourceAccesses,
String[] filterAppIds) {
List<ServicePermissionEntry> entryList = Lists.newArrayList();
if (requiredResourceAccesses == null) {
return entryList;
}
for (final RequiredResourceAccess requiredResourceAccess : requiredResourceAccesses) {
// we're interested in this resource only if it is one of the app id's in "filterAppIds"
boolean isO365Resource = Iterators.any(Iterators.forArray(filterAppIds), new Predicate<String>() {
@Override
public boolean apply(String appId) {
return requiredResourceAccess.getresourceAppId().equals(appId);
}
});
if (!isO365Resource) {
continue;
}
Office365Service service = new Office365Service();
Office365PermissionList permissions = new Office365PermissionList();
entryList.add(new ServicePermissionEntry(service, permissions));
service.setId(requiredResourceAccess.getresourceAppId());
List<ResourceAccess> resourceAccesses = requiredResourceAccess.getresourceAccess();
if (resourceAccesses == null) {
continue;
}
for (ResourceAccess resourceAccess : resourceAccesses) {
Office365Permission permission = new Office365Permission(
resourceAccess.getid().toString(),
"", "",
resourceAccess.gettype().equals("Scope")
);
permissions.add(permission);
}
}
return entryList;
}
@Override
public ListenableFuture<Application> setO365PermissionsForApp(Application application, List<ServicePermissionEntry> permissionEntryList) throws ParseException {
List<RequiredResourceAccess> requiredResourceAccesses = application.getrequiredResourceAccess();
if (requiredResourceAccesses == null) {
application.setrequiredResourceAccess(requiredResourceAccesses = Lists.newArrayList());
}
for (ServicePermissionEntry permissionEntry : permissionEntryList) {
final Office365Service service = permissionEntry.getKey();
// filter permissions for enabled permissions
Iterable<Office365Permission> permissionList = Iterables.filter(permissionEntry.getValue(), new Predicate<Office365Permission>() {
@Override
public boolean apply(Office365Permission office365Permission) {
return office365Permission.isEnabled();
}
});
// transform Office365Permission objects into ResourceAccess objects
List<ResourceAccess> resourceAccessList = Lists.newArrayList(Iterables.transform(permissionList, new Function<Office365Permission, ResourceAccess>() {
@Override
public ResourceAccess apply(Office365Permission office365Permission) {
ResourceAccess resourceAccess = new ResourceAccess();
resourceAccess.setid(UUID.fromString(office365Permission.getId()));
resourceAccess.settype("Scope");
return resourceAccess;
}
}));
// get reference to service from app in case it exists
RequiredResourceAccess requiredResourceAccess = Iterables.find(requiredResourceAccesses, new Predicate<RequiredResourceAccess>() {
@Override
public boolean apply(RequiredResourceAccess requiredResourceAccess) {
return requiredResourceAccess.getresourceAppId().equals(service.getId());
}
}, null);
if (requiredResourceAccess == null && !resourceAccessList.isEmpty()) {
requiredResourceAccesses.add(requiredResourceAccess = new RequiredResourceAccess());
requiredResourceAccess.setresourceAppId(service.getId());
}
if (requiredResourceAccess != null) {
if (resourceAccessList.isEmpty()) {
// remove requiredResourceAccess from requiredResourceAccesses
requiredResourceAccesses.remove(requiredResourceAccess);
} else {
requiredResourceAccess.setresourceAccess(resourceAccessList);
}
}
}
return updateApplication(application);
}
@Override
public ListenableFuture<Application> updateApplication(final Application application) throws ParseException {
return requestWithToken(new RequestCallback<Application>() {
@Override
public ListenableFuture<Application> execute() throws ParseException {
ApplicationFetcher appFetcher = getDirectoryClient().getapplications().getById(application.getobjectId());
return appFetcher.update(application);
}
});
}
@NotNull
@Override
public ListenableFuture<List<ServicePrincipal>> getServicePrincipals() throws ParseException {
return requestWithToken(new RequestCallback<List<ServicePrincipal>>() {
@Override
public ListenableFuture<List<ServicePrincipal>> execute() throws ParseException {
return getAllObjects(getDirectoryClient().getservicePrincipals());
}
});
}
public ListenableFuture<List<ServicePrincipal>> getServicePrincipalsForO365() throws ParseException {
return requestWithToken(new RequestCallback<List<ServicePrincipal>>() {
@Override
public ListenableFuture<List<ServicePrincipal>> execute() throws ParseException {
// build the filter
String[] appIds = new String[]{
ServiceAppIds.AZURE_ACTIVE_DIRECTORY,
ServiceAppIds.EXCHANGE,
ServiceAppIds.SHARE_POINT
};
String filter = "appId eq '" + Joiner.on("' or appId eq '").join(appIds) + "'";
return getDirectoryClient().getservicePrincipals().filter(filter).read();
}
});
}
private <T> ListenableFuture<T> getFirstItem(ListenableFuture<List<T>> future) {
return Futures.transform(future, new AsyncFunction<List<T>, T>() {
@Override
public ListenableFuture<T> apply(List<T> items) throws Exception {
return Futures.immediateFuture((items != null && items.size() > 0) ? items.get(0) : null);
}
});
}
public ListenableFuture<List<OAuth2PermissionGrant>> getPermissionGrants() throws ParseException {
return requestWithToken(new RequestCallback<List<OAuth2PermissionGrant>>() {
@Override
public ListenableFuture<List<OAuth2PermissionGrant>> execute() throws ParseException {
return getDirectoryClient().getoauth2PermissionGrants().read();
}
});
}
private <E extends DirectoryObject, F extends ODataEntityFetcher<E, ? extends DirectoryObjectOperations>, O extends ODataOperations>
ListenableFuture<List<E>> getAllObjects(final ODataCollectionFetcher<E, F, O> fetcher) {
return Futures.transform(fetcher.read(), new AsyncFunction<List<E>, List<E>>() {
@Override
public ListenableFuture<List<E>> apply(List<E> entities) throws Exception {
return Futures.successfulAsList(Lists.transform(entities, new Function<E, ListenableFuture<? extends E>>() {
@Override
public ListenableFuture<? extends E> apply(E e) {
return fetcher.getById(e.getobjectId()).read();
}
}));
}
});
}
@Override
public ListenableFuture<Application> registerApplication(@NotNull final Application application) throws ParseException {
return requestWithToken(new RequestCallback<Application>() {
@Override
public ListenableFuture<Application> execute() throws ParseException {
// register the app and then create a service principal for the app if there isn't already one
return Futures.transform(getDirectoryClient().getapplications().add(application), new AsyncFunction<Application, Application>() {
@Override
public ListenableFuture<Application> apply(final Application application) throws Exception {
return Futures.transform(getServicePrincipalsForApp(application), new AsyncFunction<List<ServicePrincipal>, Application>() {
@Override
public ListenableFuture<Application> apply(List<ServicePrincipal> servicePrincipals) throws Exception {
if (servicePrincipals.size() == 0) {
return createServicePrincipalForApp(application);
}
return Futures.immediateFuture(application);
}
});
}
});
}
});
}
private ListenableFuture<Application> createServicePrincipalForApp(final Application application) throws ParseException {
ServicePrincipal servicePrincipal = new ServicePrincipal();
servicePrincipal.setappId(application.getappId());
;
servicePrincipal.setaccountEnabled(true);
return Futures.transform(getDirectoryClient().getservicePrincipals().add(servicePrincipal), new AsyncFunction<ServicePrincipal, Application>() {
@Override
public ListenableFuture<Application> apply(ServicePrincipal servicePrincipal) throws Exception {
return Futures.immediateFuture(application);
}
});
}
@Override
public ListenableFuture<Application> getApplicationForProject(Project project) throws ParseException {
final String appId = PropertiesComponent.getInstance(project).getValue(PROJECT_APP_ID);
if (StringHelper.isNullOrWhiteSpace(appId)) {
return Futures.immediateFuture(null);
}
return requestWithToken(new RequestCallback<Application>() {
@Override
public ListenableFuture<Application> execute() throws ParseException {
return getFirstItem(
getDirectoryClient().
getapplications().
filter("appId eq '" + appId + "'").
read());
}
});
}
@Override
public void setApplicationForProject(Project project, Application application) {
PropertiesComponent.getInstance(project).setValue(PROJECT_APP_ID, application.getappId());
}
@NotNull
@Override
public ListenableFuture<List<ServicePrincipal>> getServicePrincipalsForApp(@NotNull final Application application) throws ParseException {
return requestWithToken(new RequestCallback<List<ServicePrincipal>>() {
@Override
public ListenableFuture<List<ServicePrincipal>> execute() throws ParseException {
return getDirectoryClient().
getservicePrincipals().
filter("appId eq '" + application.getappId() + "'").
read();
}
});
}
@Override
public ListenableFuture<List<ServicePrincipal>> getO365ServicePrincipalsForApp(@NotNull final Application application) throws ParseException {
return requestWithToken(new RequestCallback<List<ServicePrincipal>>() {
@Override
public ListenableFuture<List<ServicePrincipal>> execute() throws ParseException {
@SuppressWarnings("unchecked")
ListenableFuture<List<ServicePrincipal>>[] futures = new ListenableFuture[]{
getServicePrincipalsForApp(application),
getServicePrincipalsForO365()
};
final String[] filterAppIds = new String[]{
ServiceAppIds.SHARE_POINT,
ServiceAppIds.EXCHANGE,
ServiceAppIds.AZURE_ACTIVE_DIRECTORY
};
return Futures.transform(Futures.allAsList(futures), new AsyncFunction<List<List<ServicePrincipal>>, List<ServicePrincipal>>() {
@Override
public ListenableFuture<List<ServicePrincipal>> apply(List<List<ServicePrincipal>> lists) throws Exception {
// According to Guava documentation for allAsList, the list of results is in the
// same order as the input list. So first we get the service principals for the app
// filtered for O365 and Graph service principals.
final List<ServicePrincipal> servicePrincipalsForApp = Lists.newArrayList(Iterables.filter(lists.get(0), new Predicate<ServicePrincipal>() {
@Override
public boolean apply(final ServicePrincipal servicePrincipal) {
// we are only interested in O365 and Graph service principals
return Iterators.any(Iterators.forArray(filterAppIds), new Predicate<String>() {
@Override
public boolean apply(String appId) {
return appId.equals(servicePrincipal.getappId());
}
});
}
}));
// next we get the O365/graph service principals
final List<ServicePrincipal> servicePrincipalsForO365 = lists.get(1);
// then we add service principals from servicePrincipalsForO365 to servicePrincipalsForApp
// where the service principal is not available in the latter
Iterable<ServicePrincipal> servicePrincipalsToBeAdded = Iterables.filter(servicePrincipalsForO365, new Predicate<ServicePrincipal>() {
@Override
public boolean apply(ServicePrincipal servicePrincipal) {
return !servicePrincipalsForApp.contains(servicePrincipal);
}
});
Iterables.addAll(servicePrincipalsForApp, servicePrincipalsToBeAdded);
// assign the appid to the service principal and reset permissions on new service principals;
// we do Lists.newArrayList calls below to create a copy of the service lists because Lists.transform
// invokes the transformation function lazily and this causes problems for us; we force immediate
// evaluation of our transfomer by copying the elements to a new list
List<ServicePrincipal> servicePrincipals = Lists.newArrayList(Lists.transform(servicePrincipalsForApp, new Function<ServicePrincipal, ServicePrincipal>() {
@Override
public ServicePrincipal apply(ServicePrincipal servicePrincipal) {
if (!servicePrincipal.getappId().equals(application.getappId())) {
servicePrincipal.setappId(application.getappId());
servicePrincipal.setoauth2Permissions(Lists.newArrayList(Lists.transform(servicePrincipal.getoauth2Permissions(), new Function<OAuth2Permission, OAuth2Permission>() {
@Override
public OAuth2Permission apply(OAuth2Permission oAuth2Permission) {
oAuth2Permission.setisEnabled(false);
return oAuth2Permission;
}
})));
}
return servicePrincipal;
}
}));
return Futures.immediateFuture(servicePrincipals);
}
});
}
});
}
@Override
public ListenableFuture<List<ServicePrincipal>> addServicePrincipals(
@NotNull final List<ServicePrincipal> servicePrincipals) throws ParseException {
return requestWithToken(new RequestCallback<List<ServicePrincipal>>() {
@Override
public ListenableFuture<List<ServicePrincipal>> execute() throws ParseException {
List<ListenableFuture<ServicePrincipal>> futures = Lists.transform(
servicePrincipals,
new Function<ServicePrincipal, ListenableFuture<ServicePrincipal>>() {
@Override
public ListenableFuture<ServicePrincipal> apply(ServicePrincipal servicePrincipal) {
try {
return getDirectoryClient().getservicePrincipals().add(servicePrincipal);
} catch (ParseException e) {
return Futures.immediateFailedFuture(e);
}
}
}
);
return Futures.allAsList(futures);
}
});
}
private String getTenantDomain() throws ParseException {
if (authenticationToken == null) {
throw new IllegalStateException("authenticationToken is null");
}
if (tenantDomain == null) {
String upn = authenticationToken.getUserInfo().getUpn();
if (!StringHelper.isNullOrWhiteSpace(upn)) {
ArrayList<String> tokens = Lists.newArrayList(Splitter.on('@').split(upn));
if (tokens.size() != 2) {
throw new ParseException("Invalid UPN format in authentication token.", 0);
}
setTenantDomain(tokens.get(1));
}
}
return tenantDomain;
}
private void setTenantDomain(String tenantDomain) {
tenantDomainLock.lock();
this.tenantDomain = tenantDomain;
tenantDomainLock.unlock();
}
}