package com.sequenceiq.cloudbreak.service.user;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.api.client.repackaged.com.google.common.base.Strings;
import com.sequenceiq.cloudbreak.client.AccessToken;
import com.sequenceiq.cloudbreak.client.IdentityClient;
import com.sequenceiq.cloudbreak.common.type.CbUserRole;
import com.sequenceiq.cloudbreak.domain.Blueprint;
import com.sequenceiq.cloudbreak.domain.CbUser;
import com.sequenceiq.cloudbreak.domain.Credential;
import com.sequenceiq.cloudbreak.domain.Network;
import com.sequenceiq.cloudbreak.domain.Stack;
import com.sequenceiq.cloudbreak.domain.Template;
import com.sequenceiq.cloudbreak.repository.BlueprintRepository;
import com.sequenceiq.cloudbreak.repository.CredentialRepository;
import com.sequenceiq.cloudbreak.repository.NetworkRepository;
import com.sequenceiq.cloudbreak.repository.StackRepository;
import com.sequenceiq.cloudbreak.repository.TemplateRepository;
import com.sequenceiq.cloudbreak.util.JsonUtil;
@Service
public class RemoteUserDetailsService implements UserDetailsService {
private static final Logger LOGGER = LoggerFactory.getLogger(RemoteUserDetailsService.class);
private static final int ACCOUNT_PART = 2;
private static final int ROLE_PART = 2;
private static final String UAA_DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
@Value("${cb.client.secret}")
private String clientSecret;
@Inject
@Named("identityServerUrl")
private String identityServerUrl;
@Inject
private Client restClient;
@Inject
private IdentityClient identityClient;
@Inject
private StackRepository stackRepository;
@Inject
private CredentialRepository credentialRepository;
@Inject
private TemplateRepository templateRepository;
@Inject
private BlueprintRepository blueprintRepository;
@Inject
private NetworkRepository networkRepository;
private WebTarget identityWebTarget;
@PostConstruct
public void init() {
identityWebTarget = restClient.target(identityServerUrl).path("Users");
}
@Override
@Cacheable(value = "userCache", key = "#filterValue")
public CbUser getDetails(String filterValue, UserFilterField filterField) {
WebTarget target;
switch (filterField) {
case USERNAME:
target = identityWebTarget.queryParam("filter", "userName eq \"" + filterValue + "\"");
break;
case USERID:
target = identityWebTarget.path(filterValue);
break;
default:
throw new UserDetailsUnavailableException("User details cannot be retrieved.");
}
AccessToken accessToken = identityClient.getToken(clientSecret);
String scimResponse = target.request(MediaType.APPLICATION_JSON).header("Authorization", "Bearer " + accessToken.getToken()).get(String.class);
try {
JsonNode root = JsonUtil.readTree(scimResponse);
List<CbUserRole> roles = new ArrayList<>();
String account = null;
JsonNode userNode = root;
if (UserFilterField.USERNAME.equals(filterField)) {
userNode = root.get("resources").get(0);
}
for (JsonNode node : userNode.get("groups")) {
String group = node.get("display").asText();
if (group.startsWith("sequenceiq.account")) {
String[] parts = group.split("\\.");
if (account != null && !account.equals(parts[ACCOUNT_PART])) {
throw new IllegalStateException("A user can belong to only one account.");
}
account = parts[ACCOUNT_PART];
} else if (group.startsWith("sequenceiq.cloudbreak")) {
String[] parts = group.split("\\.");
roles.add(CbUserRole.fromString(parts[ROLE_PART]));
}
}
String userId = userNode.get("id").asText();
String email = userNode.get("userName").asText();
String givenName = getGivenName(userNode);
String familyName = getFamilyName(userNode);
String dateOfCreation = userNode.get("meta").get("created").asText();
Date created = parseUserCreated(dateOfCreation);
return new CbUser(userId, email, account, roles, givenName, familyName, created);
} catch (IOException e) {
throw new UserDetailsUnavailableException("User details cannot be retrieved from identity server.", e);
}
}
private String getGivenName(JsonNode userNode) {
if (userNode.get("name") != null) {
if (userNode.get("name").get("givenName") != null) {
return userNode.get("name").get("givenName").asText();
}
}
return "";
}
private String getFamilyName(JsonNode userNode) {
if (userNode.get("name") != null) {
if (userNode.get("name").get("familyName") != null) {
return userNode.get("name").get("familyName").asText();
}
}
return "";
}
@Override
@CacheEvict(value = "userCache", key = "#filterValue")
public void evictUserDetails(String updatedUserId, String filterValue) {
LOGGER.info("Remove userid: {} / username: {} from user cache", updatedUserId, filterValue);
}
@Override
@Transactional(readOnly = true)
public boolean hasResources(CbUser admin, String userId) {
CbUser user = getDetails(userId, UserFilterField.USERID);
LOGGER.info("{} / {} checks resources of {}", admin.getUserId(), admin.getUsername(), userId);
String errorMessage = null;
if (!admin.getRoles().contains(CbUserRole.ADMIN)) {
errorMessage = "Forbidden: user (%s) is not authorized for this operation on %s";
}
if (!admin.getAccount().equals(user.getAccount())) {
errorMessage = "Forbidden: admin (%s) and user (%s) are not under the same account.";
}
if (!Strings.isNullOrEmpty(errorMessage)) {
throw new AccessDeniedException(String.format(errorMessage, admin.getUsername(), user.getUsername()));
}
Set<Template> templates = templateRepository.findForUser(user.getUserId());
Set<Credential> credentials = credentialRepository.findForUser(user.getUserId());
Set<Blueprint> blueprints = blueprintRepository.findForUser(user.getUserId());
Set<Network> networks = networkRepository.findForUser(user.getUserId());
Set<Stack> stacks = stackRepository.findForUser(user.getUserId());
return !(stacks.isEmpty() && templates.isEmpty() && credentials.isEmpty()
&& blueprints.isEmpty() && networks.isEmpty());
}
private String getAuthorizationHeader(String clientId, String clientSecret) {
String creds = String.format("%s:%s", clientId, clientSecret);
try {
return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Could not convert String");
}
}
private Date parseUserCreated(String dateOfCreation) {
try {
SimpleDateFormat uaaDateFormat = new SimpleDateFormat(UAA_DATE_PATTERN);
return uaaDateFormat.parse(dateOfCreation);
} catch (ParseException e) {
throw new UserDetailsUnavailableException("User details cannot be retrieved, becuase creation date of user cannot be parsed.", e);
}
}
}