package io.cattle.platform.iaas.api.auth;
import io.cattle.platform.api.auth.Identity;
import io.cattle.platform.core.constants.AccountConstants;
import io.cattle.platform.core.constants.ProjectConstants;
import io.cattle.platform.core.dao.AccountDao;
import io.cattle.platform.core.model.Account;
import io.cattle.platform.core.model.AuthToken;
import io.cattle.platform.core.util.SettingsUtils;
import io.cattle.platform.iaas.api.auth.dao.AuthDao;
import io.cattle.platform.iaas.api.auth.dao.AuthTokenDao;
import io.cattle.platform.iaas.api.auth.identity.Token;
import io.cattle.platform.iaas.api.auth.integration.interfaces.TokenUtil;
import io.cattle.platform.iaas.api.auth.projects.ProjectResourceManager;
import io.cattle.platform.object.ObjectManager;
import io.cattle.platform.object.util.DataAccessor;
import io.cattle.platform.token.TokenException;
import io.cattle.platform.token.TokenService;
import io.cattle.platform.util.type.CollectionUtils;
import io.github.ibuildthecloud.gdapi.context.ApiContext;
import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException;
import io.github.ibuildthecloud.gdapi.request.ApiRequest;
import io.github.ibuildthecloud.gdapi.util.ResponseCodes;
import io.github.ibuildthecloud.gdapi.validation.ValidationErrorCodes;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import javax.servlet.http.Cookie;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class AbstractTokenUtil implements TokenUtil {
public static final String ACCESSMODE = "accessMode";
public static final String TOKEN = "token";
public static final String ACCOUNT_ID = "account_id";
public static final String ACCESS_TOKEN_INVALID = "InvalidAccessToken";
public static final String ID_LIST = "idList";
public static final String USER_IDENTITY = "userIdentity";
public static final String USER_TYPE = "userType";
public static final String REQUIRED_ACCESSMODE = "required";
public static final String RESTRICTED_ACCESSMODE = "restricted";
public static final String UNRESTRICTED_ACCESSMODE = "unrestricted";
private static final Logger log = LoggerFactory.getLogger(AbstractTokenUtil.class);
@Inject
protected AuthDao authDao;
@Inject
TokenService tokenService;
@Inject
ProjectResourceManager projectResourceManager;
@Inject
AuthTokenDao authTokenDao;
@Inject
ObjectManager objectManager;
@Inject
SettingsUtils settingsUtils;
@Inject
AccountDao accountDao;
@Override
public Account getAccountFromJWT() {
Map<String, Object> jsonData = getJsonData();
if (jsonData == null) {
return null;
}
String accountId = ObjectUtils.toString(jsonData.get(ACCOUNT_ID), null);
if (null == accountId) {
return null;
}
Account account = authDao.getAccountByExternalId(accountId, userType());
if (account != null && !accountDao.isActiveAccount(account)) {
throw new ClientVisibleException(ResponseCodes.UNAUTHORIZED);
}
return account;
}
protected Map<String, Object> getJsonData() {
return getJsonData(getJWT(), tokenType());
}
private Map<String, Object> getJsonData(String jwtKey, String tokenType) {
if (StringUtils.isEmpty(jwtKey)){
return null;
}
String toParse;
toParse = removeBearer(jwtKey);
if (StringUtils.isBlank(toParse)) {
return null;
}
String dbJwt = retrieveJwt(toParse);
if (StringUtils.isNotBlank(dbJwt)){
toParse = dbJwt;
}
toParse = removeBearer(toParse);
if (StringUtils.isEmpty(toParse)) {
return null;
}
Map<String, Object> jsonData;
try {
jsonData = tokenService.getJsonPayload(toParse, true);
} catch (TokenException e) { // in case of invalid token
return null;
}
if (jsonData == null) {
throw new ClientVisibleException(ResponseCodes.BAD_REQUEST, ACCESS_TOKEN_INVALID,
"Json Web Token invalid.", null);
}
String tokenTypeActual = (String) jsonData.get(TOKEN);
if (!StringUtils.equals(tokenType, tokenTypeActual)) {
return null;
}
if (!isAllowed(jsonData)) {
throw new ClientVisibleException(ResponseCodes.UNAUTHORIZED);
}
return jsonData;
}
private String removeBearer(String jwtKey) {
String toParse;
String[] tokenArr = jwtKey.split("\\s+");
if (tokenArr.length == 2) {
if (!StringUtils.equalsIgnoreCase("bearer", StringUtils.trim(tokenArr[0]))) {
return null;
}
toParse = tokenArr[1];
} else if (tokenArr.length == 1) {
toParse = tokenArr[0];
} else {
toParse = jwtKey;
}
return toParse;
}
private String retrieveJwt(String jwtKey) {
AuthToken authToken= authTokenDao.getTokenByKey(jwtKey);
if (authToken == null) {
return null;
}
if (!authToken.getProvider().equalsIgnoreCase(SecurityConstants.AUTH_PROVIDER.get())){
throw new ClientVisibleException(ResponseCodes.UNAUTHORIZED, "AuthProviderChanged",
"Access control has changed since token was created.", null);
}
return authToken.getValue();
}
@Override
public String getJWT() {
ApiRequest request = ApiContext.getContext().getApiRequest();
String jwt = (String) request.getAttribute(tokenType());
if (StringUtils.isNotBlank(jwt) && getJsonData(jwt, tokenType()) == null) {
throw new ClientVisibleException(ResponseCodes.BAD_REQUEST, ValidationErrorCodes.INVALID_FORMAT,
"Token malformed after retrieval.", null);
}
return jwt;
}
@SuppressWarnings("unchecked")
protected boolean isAllowed(Map<String, Object> jsonData) {
List<String> idList = (List<String>) jsonData.get(ID_LIST);
log.trace("ID List in the token: {}", idList);
Set<Identity> identities = identities(jsonData);
return isAllowed(idList, identities);
}
@SuppressWarnings("unchecked")
protected Set<Identity> identities(Map<String, Object> jsonData) {
Set<Identity> identities = new HashSet<>();
if (jsonData == null) {
return identities;
}
List<String> idList = (List<String>) jsonData.get(ID_LIST);
for (String id : idList) {
Identity identityObj = Identity.fromId(id);
if (identityObj != null) {
identities.add(identityObj);
} else {
log.trace("Identity is null for id: {}", id);
}
}
return identities;
}
@Override
public Set<Identity> getIdentities() {
Map<String, Object> jsonData = getJsonData();
if (jsonData == null) {
return new HashSet<>();
}
return identities(jsonData);
}
@Override
public boolean findAndSetJWT() {
ApiRequest request = ApiContext.getContext().getApiRequest();
String jwt = (String) request.getAttribute(tokenType());
if (StringUtils.isNotBlank(jwt) && getJsonData(jwt, tokenType()) != null) {
return true;
}
if (StringUtils.isBlank(jwt)) {
Cookie[] cookies = request.getServletContext().getRequest().getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase(TOKEN)
&& StringUtils.isNotBlank(cookie.getValue())) {
jwt = cookie.getValue();
break;
}
}
}
}
if (StringUtils.isBlank(jwt)) {
jwt = request.getServletContext().getRequest().getHeader(ProjectConstants.AUTH_HEADER);
}
if (StringUtils.isBlank(jwt)) {
jwt = request.getServletContext().getRequest().getParameter(TOKEN);
}
if (getJsonData(jwt, tokenType()) != null) {
request.setAttribute(tokenType(), jwt);
return true;
}
return false;
}
@Override
public boolean isAllowed(List<String> idList, Set<Identity> identities) {
switch (accessMode()) {
case REQUIRED_ACCESSMODE:
if (isWhitelisted(idList)) {
break;
}
throw new ClientVisibleException(ResponseCodes.UNAUTHORIZED);
case RESTRICTED_ACCESSMODE:
boolean hasAccessToAProject = authDao.hasAccessToAnyProject(identities, false, null);
if (hasAccessToAProject || isWhitelisted(idList)) {
break;
}
throw new ClientVisibleException(ResponseCodes.UNAUTHORIZED);
case UNRESTRICTED_ACCESSMODE:
break;
default:
throw new ClientVisibleException(ResponseCodes.UNAUTHORIZED);
}
return true;
}
protected abstract boolean isWhitelisted(List<String> idList);
protected abstract String accessMode();
@Override
public String getAccessToken() {
if (findAndSetJWT()) {
Account account = getAccountFromJWT();
return account != null ? (String) DataAccessor.fields(account).withKey(accessToken()).get() : null;
} else {
return null;
}
}
@Override
public List<String> identitiesToIdList(Set<Identity> identities){
List<String> idList = new ArrayList<>();
for (Identity identity: identities){
idList.add(identity.getId());
}
return idList;
}
protected abstract String accessToken();
@Override
public Account getOrCreateAccount(Identity user, Set<Identity> identities, Account account) {
if (SecurityConstants.SECURITY.get()) {
isAllowed(identitiesToIdList(identities), identities);
if (account == null) {
account = authDao.getAccountByExternalId(user.getExternalId(), user.getExternalIdType());
}
if (account != null && !accountDao.isActiveAccount(account)) {
throw new ClientVisibleException(ResponseCodes.UNAUTHORIZED);
}
if (account == null && createAccount()) {
account = authDao.createAccount(user.getName(), AccountConstants.USER_KIND, user
.getExternalId(),
user.getExternalIdType());
}
Object hasLoggedIn = DataAccessor.fields(account).withKey(SecurityConstants.HAS_LOGGED_IN).get();
if ((hasLoggedIn == null || !((Boolean) hasLoggedIn)) &&
!authDao.hasAccessToAnyProject(identities, false, null)) {
projectResourceManager.createProjectForUser(user);
}
} else {
if (account == null) {
account = authDao.getAccountByExternalId(user.getExternalId(), user.getExternalIdType());
}
if (account != null){
account.setKind(AccountConstants.ADMIN_KIND);
objectManager.persist(account);
} else {
account = authDao.getAdminAccount();
}
authDao.ensureAllProjectsHaveNonRancherIdMembers(user);
settingsUtils.changeSetting(SecurityConstants.AUTH_ENABLER, user.getId());
}
if (account != null) {
DataAccessor.fields(account).withKey(SecurityConstants.HAS_LOGGED_IN).set(true);
objectManager.persist(account);
}
return account;
}
@Override
public Token createToken(Set<Identity> identities, Account account) {
Identity user = getUser(identities);
if (user == null) {
throw new ClientVisibleException(ResponseCodes.UNAUTHORIZED);
}
account = getOrCreateAccount(user, identities, account);
if (account == null){
throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR, "FailedToGetAccount");
}
postAuthModification(account);
account = authDao.updateAccount(account, user.getName(), account.getKind(), user.getExternalId(), user
.getExternalIdType());
Map<String, Object> jsonData = new HashMap<>();
jsonData.put(AbstractTokenUtil.TOKEN, tokenType());
jsonData.put(AbstractTokenUtil.ACCOUNT_ID, user.getExternalId());
jsonData.put(AbstractTokenUtil.ID_LIST, identitiesToIdList(identities));
jsonData.put(AbstractTokenUtil.USER_IDENTITY, user);
jsonData.put(AbstractTokenUtil.USER_TYPE, account.getKind());
String accountId = (String) ApiContext.getContext().getIdFormatter().formatId(objectManager.getType(Account.class), account.getId());
Date expiry = new Date(System.currentTimeMillis() + SecurityConstants.TOKEN_EXPIRY_MILLIS.get());
String jwt = tokenService.generateEncryptedToken(jsonData, expiry);
return new Token(jwt, accountId, user, new ArrayList<>(identities), account.getKind());
}
protected abstract void postAuthModification(Account account);
@Override
public Identity getUser(Set<Identity> identities) {
for (Identity identity: identities){
if (identity != null && identity.getExternalIdType().equalsIgnoreCase(userType())){
return identity;
}
}
return null;
}
@Override
public ObjectManager getObjectManager() {
return objectManager;
}
@Override
public boolean isConfigured() {
return StringUtils.isNotBlank(SecurityConstants.AUTH_PROVIDER.get())
&& getName().equalsIgnoreCase(SecurityConstants.AUTH_PROVIDER.get());
}
public static boolean isRestrictedAccess(String accessMode){
return RESTRICTED_ACCESSMODE.equalsIgnoreCase(accessMode);
}
public static boolean isRequiredAccess(String accessMode){
return REQUIRED_ACCESSMODE.equalsIgnoreCase(accessMode);
}
public static boolean isUnrestrictedAccess(String accessMode){
return UNRESTRICTED_ACCESSMODE.equalsIgnoreCase(accessMode);
}
public Identity jsonToIdentity(Map<String, Object> jsonData) {
String externalId = ObjectUtils.toString(jsonData.get("externalId"));
String externalIdType = ObjectUtils.toString(jsonData.get("externalIdType"));
String name = ObjectUtils.toString(jsonData.get("name"));
String profilePicture = ObjectUtils.toString(jsonData.get("profilePicture"));
String profileUrl = ObjectUtils.toString(jsonData.get("profileUrl"));
String login = ObjectUtils.toString(jsonData.get("login"));
return new Identity(externalIdType, externalId, name, profileUrl, profilePicture, login);
}
public Token getUserIdentityFromJWT() {
Token token = new Token();
Map<String, Object> jsonData = getJsonData();
if (jsonData == null) {
return null;
}
Object idObject = jsonData.get(USER_IDENTITY);
if (idObject != null) {
Map<String, Object> idMap = CollectionUtils.toMap(idObject);
Identity userIdentity = jsonToIdentity(idMap);
String userType = ObjectUtils.toString(jsonData.get(USER_TYPE), null);
token.setUserIdentity(userIdentity);
token.setUserType(userType);
}
return token;
}
public Token retrieveCurrentToken() {
Token token = new Token();
if (findAndSetJWT()) {
Token userToken = getUserIdentityFromJWT();
if(userToken != null) {
token.setUserIdentity(userToken.getUserIdentity());
token.setUserType(userToken.getUserType());
token.setJwt(getJWT());
}
}
log.debug("retrieveCurrentToken returning {}", token);
return token;
}
}