package io.cattle.platform.iaas.api.auth.integration.azure;
import io.cattle.platform.api.auth.Identity;
import io.cattle.platform.json.JsonMapper;
import io.github.ibuildthecloud.gdapi.context.ApiContext;
import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException;
import io.github.ibuildthecloud.gdapi.util.ResponseCodes;
import io.cattle.platform.util.type.CollectionUtils;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.fluent.Request;
import org.apache.http.entity.ContentType;
public class AzureRESTClient extends AzureConfigurable{
@Inject
private JsonMapper jsonMapper;
@Inject
private AzureTokenUtil azureTokenUtil;
private static final Log logger = LogFactory.getLog(AzureRESTClient.class);
private Identity getUserIdentity(String azureAccessToken) {
if (StringUtils.isEmpty(azureAccessToken)) {
noAccessToken();
}
try {
logger.debug("getUserIdentity for logged in user");
HttpResponse response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.USER, ""));
if(hasTokenExpired(response)) {
//refresh token
refreshAccessToken();
//retry the request
azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.USER, ""));
}
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode >= 300) {
noAzure(response);
}
Map<String, Object> jsonData = jsonMapper.readValue(response.getEntity().getContent());
return jsonToAzureAccountInfo(jsonData).toIdentity(AzureConstants.USER_SCOPE);
} catch (IOException e) {
logger.error("Failed to get Azure user account info.", e);
throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR, AzureConstants.AZURE_CLIENT,
"Failed to get Azure user account info.", null);
} catch(ClientVisibleException ex) {
logger.error("Failed to get Azure user account info.", ex);
throw ex;
} catch(Exception ex) {
logger.error("Failed to get Azure user account info.", ex);
throw new RuntimeException(ex);
}
}
private List<Identity> getGroupIdentities(String azureAccessToken) {
if (StringUtils.isEmpty(azureAccessToken)) {
noAccessToken();
}
try {
logger.debug("getGroupIdentities for logged in user");
HttpResponse response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.GROUP, ""));
if(hasTokenExpired(response)) {
//refresh token
refreshAccessToken();
//retry the request
azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.GROUP, ""));
}
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode >= 300) {
noAzure(response);
}
Map<String, Object> jsonData = CollectionUtils.toMap(jsonMapper.readValue(response.getEntity().getContent(), Map.class));
List<AzureAccountInfo> searchResponseList = parseSearchResponseList(jsonData);
List<Identity> groupIdentities = new ArrayList<Identity>();
if(searchResponseList != null && !searchResponseList.isEmpty()){
for(AzureAccountInfo userOrGroup : searchResponseList){
groupIdentities.add(userOrGroup.toIdentity(AzureConstants.GROUP_SCOPE));
}
}
return groupIdentities;
} catch (IOException e) {
logger.error("Failed to get Azure group memberships.", e);
throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR, AzureConstants.AZURE_CLIENT,
"Failed to get Azure group memberships.", null);
} catch(ClientVisibleException ex) {
logger.error("Failed to get Azure group memberships.", ex);
throw ex;
} catch(Exception ex) {
logger.error("Failed to get Azure group memberships.", ex);
throw new RuntimeException(ex);
}
}
public String getAccessToken(String code) {
if (!isConfigured()) {
throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR, AzureConstants.CONFIG, "Azure Client and Tenant Id not configured", null);
}
logger.debug("getAccessToken from Azure");
String[] split = code.split(":", 2);
if (split.length != 2) {
throw new ClientVisibleException(ResponseCodes.BAD_REQUEST, "MalformedCode");
}
String username = split[0];
String domain = AzureConstants.AZURE_DOMAIN.get();
//check if the username has a domain, if not then append the domain.
if(domain != null && domain != "" && !username.endsWith("@"+domain)) {
username = username + "@"+ domain;
}
//testuser1%40ZTAD.onmicrosoft.com&password=testpwd1%21";
String accessToken, refreshToken;
try{
StringBuilder body = new StringBuilder("scope=openid&grant_type=password&resource=https%3A%2F%2Fgraph.windows.net");
body.append("&client_id=");
body.append(URLEncoder.encode(AzureConstants.AZURE_CLIENT_ID.get(), "UTF-8"));
body.append("&username=");
body.append(URLEncoder.encode(username, "UTF-8"));
body.append("&password=");
body.append(URLEncoder.encode(split[1], "UTF-8"));
HttpResponse response = Request.Post(AzureConstants.AUTHORITY)
.addHeader(AzureConstants.ACCEPT, AzureConstants.APPLICATION_FORM_URL_ENCODED)
.bodyString(body.toString(), ContentType.APPLICATION_FORM_URLENCODED).execute().returnResponse();
int statusCode = response.getStatusLine().getStatusCode();
if(statusCode >= 300) {
noAzure(response);
}
Map<String, Object> jsonData = jsonMapper.readValue(response.getEntity().getContent());
accessToken = ObjectUtils.toString(jsonData.get("access_token"));
refreshToken = ObjectUtils.toString(jsonData.get("refresh_token"));
ApiContext.getContext().getApiRequest().setAttribute(AzureConstants.AZURE_ACCESS_TOKEN, accessToken);
ApiContext.getContext().getApiRequest().setAttribute(AzureConstants.AZURE_REFRESH_TOKEN, refreshToken);
} catch(ClientVisibleException ex) {
throw ex;
} catch(Exception ex) {
throw new RuntimeException(ex);
}
return accessToken;
}
public void refreshAccessToken() {
if (!isConfigured()) {
throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR, AzureConstants.CONFIG, "Azure Client and Tenant Id not configured", null);
}
logger.debug("Azure accessToken expired, refreshing the token");
String azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
if (StringUtils.isEmpty(azureAccessToken)) {
noAccessToken();
}
String azureRefreshToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_REFRESH_TOKEN);
if (StringUtils.isEmpty(azureRefreshToken)) {
noRefreshToken();
}
try{
//post to the microsoft azure authority
HttpResponse response = Request.Post(AzureConstants.AUTHORITY)
.addHeader(AzureConstants.ACCEPT, AzureConstants.APPLICATION_FORM_URL_ENCODED)
.bodyString("grant_type=refresh_token&refresh_token="+azureRefreshToken, ContentType.APPLICATION_FORM_URLENCODED)
.execute().returnResponse();
int statusCode = response.getStatusLine().getStatusCode();
if(statusCode >= 300) {
noAzure(response);
}
Map<String, Object> jsonData = jsonMapper.readValue(response.getEntity().getContent());
String newAccessToken = ObjectUtils.toString(jsonData.get("access_token"));
String newRefreshToken = ObjectUtils.toString(jsonData.get("refresh_token"));
ApiContext.getContext().getApiRequest().setAttribute(AzureConstants.AZURE_ACCESS_TOKEN, newAccessToken);
ApiContext.getContext().getApiRequest().setAttribute(AzureConstants.AZURE_REFRESH_TOKEN, newRefreshToken);
logger.debug("Storing the new Azure access token to the Account");
azureTokenUtil.refreshAccessToken();
} catch(ClientVisibleException ex) {
logger.error("Failed to refreshAccessToken for Azure user.", ex);
throw ex;
} catch(Exception ex) {
logger.error("Failed to refreshAccessToken for Azure user.", ex);
throw new RuntimeException(ex);
}
}
public List<AzureAccountInfo> parseSearchResponseList(Map<String, Object> jsonData) {
List<?> azureValueList = CollectionUtils.toList(jsonData.get("value"));
List<AzureAccountInfo> azureUserOrGroupList = new ArrayList<AzureAccountInfo>();
if (azureValueList != null && !azureValueList.isEmpty())
{
for(Object azureValue : azureValueList) {
Map<String, Object> result = CollectionUtils.toMap(azureValue);
String objectType = ObjectUtils.toString(result.get("objectType"));
if(objectType != null && (objectType.equalsIgnoreCase("User") || objectType.equalsIgnoreCase("Group"))) {
AzureAccountInfo userOrGroupInfo = jsonToAzureAccountInfo(result);
azureUserOrGroupList.add(userOrGroupInfo);
}
}
}
return azureUserOrGroupList;
}
public AzureAccountInfo jsonToAzureAccountInfo(Map<String, Object> jsonData) {
String objectId = ObjectUtils.toString(jsonData.get("objectId"));
String objectType = ObjectUtils.toString(jsonData.get("objectType"));
String userPrincipalName = ObjectUtils.toString(jsonData.get("userPrincipalName"));
String accountName = ObjectUtils.toString(jsonData.get("mailNickname"));
if("Group".equalsIgnoreCase(objectType)) {
accountName = ObjectUtils.toString(jsonData.get("displayName"));
}
String name = ObjectUtils.toString(jsonData.get("displayName"));
if (StringUtils.isBlank(name)) {
name = accountName;
}
//String profilePicture = ObjectUtils.toString(jsonData.get(GithubConstants.PROFILE_PICTURE));
return new AzureAccountInfo(objectId, accountName, null, userPrincipalName, name);
}
public boolean hasTokenExpired(HttpResponse response) throws IOException {
int statusCode = response.getStatusLine().getStatusCode();
if(statusCode == 401) {
//if token expired, refresh token.
Map<String, Object> jsonData = jsonMapper.readValue(response.getEntity().getContent());
Map<String, Object> azureError = CollectionUtils.toMap(jsonData.get("odata.error"));
String azureCode = ObjectUtils.toString(azureError.get("code"));
if("Authentication_ExpiredToken".equalsIgnoreCase(azureCode)) {
return true;
}
}
return false;
}
public HttpResponse getFromAzure(String azureAccessToken, String url) throws IOException {
HttpResponse response = Request.Get(url).addHeader(AzureConstants.AUTHORIZATION, "Bearer " +
"" + azureAccessToken).addHeader(AzureConstants.ACCEPT, AzureConstants.APPLICATION_JSON).execute().returnResponse();
logger.debug("Response from Azure API: "+ response.getStatusLine());
return response;
}
private void noAccessToken() {
throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR,
"AzureAccessToken", "No Azure Access token", null);
}
private void noRefreshToken() {
throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR,
"AzureRefreshToken", "No Azure Refresh token", null);
}
public void noAzure(HttpResponse response) {
int statusCode = response.getStatusLine().getStatusCode();
try {
Map<String, Object> jsonData = jsonMapper.readValue(response.getEntity().getContent());
String azureError = (String)jsonData.get("error");
String azureErrorDesc = (String)jsonData.get("error_description");
if("invalid_grant".equalsIgnoreCase(azureError) || "unauthorized_client".equalsIgnoreCase(azureError)) {
throw new ClientVisibleException(ResponseCodes.UNAUTHORIZED);
}
logger.error("Error received from Azure: " + azureError);
logger.error("Error Description received from Azure: " + azureErrorDesc);
throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE, AzureConstants.AZURE_ERROR,
"Error from Azure: " + Integer.toString(statusCode), "Details: " +azureError + ", Description: "+azureErrorDesc);
} catch(ClientVisibleException ex) {
logger.error("Got error from Azure.", ex);
throw ex;
} catch(Exception ex) {
throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE, AzureConstants.AZURE_ERROR,
"Error Response from Azure", "Status code from Azure: " + Integer.toString(statusCode));
}
}
public String getURL(AzureClientEndpoints val, String objectId) {
String apiEndpoint = AzureConstants.GRAPH_API_ENDPOINT;
String tenantId = AzureConstants.AZURE_TENANT_ID.get();
String toReturn;
switch (val) {
case USERS:
toReturn = apiEndpoint + tenantId + "/users/" + objectId;
break;
case GROUPS:
toReturn = apiEndpoint + tenantId + "/groups/" + objectId;
break;
case USER:
toReturn = apiEndpoint + "me";
break;
case GROUP:
toReturn = apiEndpoint + "me/memberOf";
break;
default:
throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR,
"AzureClient", "Attempted to get invalid Api endpoint.", null);
}
return toReturn + AzureConstants.GRAPH_API_VERSION;
}
public AzureAccountInfo getAzureUserByName(String username) {
String azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
if (StringUtils.isEmpty(azureAccessToken)) {
noAccessToken();
}
logger.debug("getAzureUserByName: "+ username);
try {
if (StringUtils.isEmpty(username)) {
throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR,
"getAzureUser", "No azure username specified.", null);
}
String domain = AzureConstants.AZURE_DOMAIN.get();
//check if the username has a domain, if not then append the domain.
if(domain != null && domain != "" && !username.endsWith("@"+domain)) {
username = username + "@"+ domain;
}
username = URLEncoder.encode(username, "UTF-8");
String filter = "$filter=userPrincipalName%20eq%20'" + username + "'";
//String filter = "$filter=startswith(userPrincipalName,'" + username+ "')";
HttpResponse response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.USERS, "") + "&"+ filter);
if(hasTokenExpired(response)) {
//refresh token
refreshAccessToken();
//retry the request
azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.USERS, "") + "&"+ filter);
}
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode >= 300) {
noAzure(response);
}
Map<String, Object> jsonData = CollectionUtils.toMap(jsonMapper.readValue(response.getEntity().getContent(), Map.class));
List<AzureAccountInfo> searchResponseList = parseSearchResponseList(jsonData);
if(searchResponseList != null && !searchResponseList.isEmpty()) {
return searchResponseList.get(0);
}
return null;
} catch (IOException e) {
logger.error(e);
throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE, "AzureUnavailable", "Could not retrieve User by name from Azure", null);
} catch(ClientVisibleException ex) {
logger.error("Failed to get Azure user account info by name.", ex);
throw ex;
} catch(Exception ex) {
logger.error("Failed to get Azure user account info by name.", ex);
throw new RuntimeException(ex);
}
}
public AzureAccountInfo getAzureGroupByName(String org) {
String azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
if (StringUtils.isEmpty(azureAccessToken)) {
noAccessToken();
}
logger.debug("getAzureGroupByName: "+ org);
try {
if (StringUtils.isEmpty(org)) {
throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR,
"noAzureGroupName", "No group name specified when retrieving from Azure.", null);
}
org = URLEncoder.encode(org, "UTF-8");
String filter = "$filter=displayName%20eq%20'" + org + "'";
//String filter = "$filter=startswith(displayName,'" + org+ "')";
HttpResponse response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.GROUPS, "") + "&"+ filter);
if(hasTokenExpired(response)) {
//refresh token
refreshAccessToken();
//retry the request
azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.GROUPS, "") + "&"+ filter);
}
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode >= 300) {
noAzure(response);
}
Map<String, Object> jsonData = CollectionUtils.toMap(jsonMapper.readValue(response.getEntity().getContent
(), Map.class));
List<AzureAccountInfo> searchResponseList = parseSearchResponseList(jsonData);
if(searchResponseList != null && !searchResponseList.isEmpty()) {
return searchResponseList.get(0);
}
return null;
} catch (IOException e) {
logger.error(e);
throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE, "AzureUnavailable", "Could not retrieve Group by name from Azure", null);
} catch(ClientVisibleException ex) {
logger.error("Failed to get Azure group info by name.", ex);
throw ex;
} catch(Exception ex) {
logger.error("Failed to get Azure group info by name.", ex);
throw new RuntimeException(ex);
}
}
public AzureAccountInfo getUserById(String id) {
String azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
if (StringUtils.isEmpty(azureAccessToken)) {
noAccessToken();
}
logger.debug("getUserById: "+ id);
try {
HttpResponse response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.USERS, id));
if(hasTokenExpired(response)) {
//refresh token
refreshAccessToken();
//retry the request
azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.USERS, id));
}
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode >= 300) {
noAzure(response);
}
Map<String, Object> jsonData = CollectionUtils.toMap(jsonMapper.readValue(response.getEntity().getContent(), Map.class));
return jsonToAzureAccountInfo(jsonData);
} catch (IOException e) {
throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE, "AzureUnavailable", "Could not retrieve User by Id from Azure", null);
} catch(ClientVisibleException ex) {
logger.error("Failed to get Azure user account info by Id.", ex);
throw ex;
} catch(Exception ex) {
logger.error("Failed to get Azure user account info by Id.", ex);
throw new RuntimeException(ex);
}
}
public AzureAccountInfo getGroupById(String id) {
String azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
if (StringUtils.isEmpty(azureAccessToken)) {
noAccessToken();
}
logger.debug("getGroupById: "+ id);
try {
HttpResponse response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.GROUPS, id));
if(hasTokenExpired(response)) {
//refresh token
refreshAccessToken();
//retry the request
azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.GROUPS, id));
}
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode >= 300) {
noAzure(response);
}
Map<String, Object> jsonData = CollectionUtils.toMap(jsonMapper.readValue(response.getEntity().getContent(), Map.class));
return jsonToAzureAccountInfo(jsonData);
} catch (IOException e) {
throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE, "AzureUnavailable", "Could not retrieve Group by Id from Azure", null);
} catch(ClientVisibleException ex) {
logger.error("Failed to get Azure group info by id.", ex);
throw ex;
} catch(Exception ex) {
logger.error("Failed to get Azure group info by id.", ex);
throw new RuntimeException(ex);
}
}
public Set<Identity> getIdentities(String accessToken) {
Set<Identity> identities = new HashSet<>();
identities.add(getUserIdentity(accessToken));
identities.addAll(getGroupIdentities(accessToken));
return identities;
}
@Override
public String getName() {
return AzureConstants.AZURE_CLIENT;
}
}