package org.zstack.identity;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.transaction.annotation.Transactional;
import org.zstack.core.Platform;
import org.zstack.core.cloudbus.CloudBus;
import org.zstack.core.cloudbus.EventCallback;
import org.zstack.core.cloudbus.EventFacade;
import org.zstack.core.cloudbus.MessageSafe;
import org.zstack.core.componentloader.PluginRegistry;
import org.zstack.core.config.*;
import org.zstack.core.db.*;
import org.zstack.core.db.SimpleQuery.Op;
import org.zstack.core.errorcode.ErrorFacade;
import org.zstack.core.thread.PeriodicTask;
import org.zstack.core.thread.ThreadFacade;
import org.zstack.header.AbstractService;
import org.zstack.header.apimediator.ApiMessageInterceptionException;
import org.zstack.header.apimediator.ApiMessageInterceptor;
import org.zstack.header.apimediator.GlobalApiMessageInterceptor;
import org.zstack.header.errorcode.OperationFailureException;
import org.zstack.header.errorcode.SysErrors;
import org.zstack.header.exception.CloudRuntimeException;
import org.zstack.header.identity.*;
import org.zstack.header.identity.AccountConstant.StatementEffect;
import org.zstack.header.identity.IdentityCanonicalEvents.AccountDeletedData;
import org.zstack.header.identity.IdentityCanonicalEvents.UserDeletedData;
import org.zstack.header.identity.PolicyInventory.Statement;
import org.zstack.header.identity.Quota.QuotaPair;
import org.zstack.header.managementnode.PrepareDbInitialValueExtensionPoint;
import org.zstack.header.message.*;
import org.zstack.header.notification.ApiNotification;
import org.zstack.header.notification.ApiNotificationFactory;
import org.zstack.header.notification.ApiNotificationFactoryExtensionPoint;
import org.zstack.header.search.APIGetMessage;
import org.zstack.header.search.APISearchMessage;
import org.zstack.header.vo.APIGetResourceNamesMsg;
import org.zstack.header.vo.APIGetResourceNamesReply;
import org.zstack.header.vo.ResourceInventory;
import org.zstack.header.vo.ResourceVO;
import org.zstack.utils.*;
import org.zstack.utils.function.ForEachFunction;
import org.zstack.utils.gson.JSONObjectUtil;
import org.zstack.utils.logging.CLogger;
import org.zstack.utils.path.PathUtil;
import static org.zstack.core.Platform.argerr;
import static org.zstack.core.Platform.operr;
import javax.persistence.Query;
import javax.persistence.Tuple;
import javax.persistence.TypedQuery;
import java.io.File;
import java.lang.reflect.Field;
import java.sql.Timestamp;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.zstack.utils.CollectionDSL.list;
public class AccountManagerImpl extends AbstractService implements AccountManager, PrepareDbInitialValueExtensionPoint,
SoftDeleteEntityExtensionPoint, HardDeleteEntityExtensionPoint,
GlobalApiMessageInterceptor, ApiMessageInterceptor, ApiNotificationFactoryExtensionPoint {
private static final CLogger logger = Utils.getLogger(AccountManagerImpl.class);
@Autowired
private CloudBus bus;
@Autowired
private DatabaseFacade dbf;
@Autowired
private DbEntityLister dl;
@Autowired
private ErrorFacade errf;
@Autowired
private ThreadFacade thdf;
@Autowired
private PluginRegistry pluginRgty;
@Autowired
private EventFacade evtf;
@Autowired
private GlobalConfigFacade gcf;
private List<String> resourceTypeForAccountRef;
private List<Class> resourceTypes;
private Map<String, SessionInventory> sessions = new ConcurrentHashMap<>();
private Map<Class, List<Quota>> messageQuotaMap = new HashMap<>();
private Map<String, Quota> nameQuotaMap = new HashMap<>();
private HashSet<Class> accountApiControl = new HashSet<>();
private HashSet<Class> accountApiControlInternal = new HashSet<>();
private List<Quota> definedQuotas = new ArrayList<>();
@Override
public Map<Class, ApiNotificationFactory> apiNotificationFactory() {
Map<Class, ApiNotificationFactory> factories = new HashMap<>();
factories.put(APIChangeResourceOwnerMsg.class, new ApiNotificationFactory() {
@Override
public ApiNotification createApiNotification(APIMessage msg) {
APIChangeResourceOwnerMsg cmsg = (APIChangeResourceOwnerMsg) msg;
return new ApiNotification() {
String originAccountUuid;
String resourceType;
@Override
public void before() {
Tuple tuple = Q.New(AccountResourceRefVO.class)
.select(AccountResourceRefVO_.accountUuid, AccountResourceRefVO_.resourceType)
.eq(AccountResourceRefVO_.resourceUuid, cmsg.getResourceUuid()).findTuple();
originAccountUuid = tuple.get(0, String.class);
resourceType = tuple.get(1, String.class);
}
@Override
public void after(APIEvent evt) {
ntfy("changing the ownership to the account[uuid:%s]", cmsg.getAccountUuid())
.resource(cmsg.getResourceUuid(), resourceType)
.context("srcAccountUuid", originAccountUuid)
.context("dstAccountUuid", cmsg.getAccountUuid())
.messageAndEvent(cmsg, evt)
.done();
ntfy("transferring a resource[type: %s, uuid: %s] to the account[uuid:%s]", resourceType, cmsg.getResourceUuid(), cmsg.getAccountUuid())
.resource(originAccountUuid, AccountVO.class.getSimpleName())
.context("dstAccountUuid", cmsg.getAccountUuid())
.context("resourceUuid", cmsg.getResourceUuid())
.context("resourceType", resourceType)
.messageAndEvent(cmsg, evt)
.done();
ntfy("receiving a resource[type: %s, uuid: %s] from the account[uuid:%s]", resourceType, cmsg.getResourceUuid(), originAccountUuid)
.resource(originAccountUuid, AccountVO.class.getSimpleName())
.context("srcAccountUuid", cmsg.getAccountUuid())
.context("resourceUuid", cmsg.getResourceUuid())
.context("resourceType", resourceType)
.messageAndEvent(cmsg, evt)
.done();
}
};
}
});
// TODO: handle APIRevokeResourceSharingMsg
// TODO: handle APIShareResourceMsg
return factories;
}
class AccountCheckField {
Field field;
APIParam param;
}
class MessageAction {
boolean adminOnly;
List<String> actions;
String category;
boolean accountOnly;
List<AccountCheckField> accountCheckFields;
boolean accountControl;
}
private Map<Class, MessageAction> actions = new HashMap<>();
private Future<Void> expiredSessionCollector;
@Override
@MessageSafe
public void handleMessage(Message msg) {
if (msg instanceof AccountMessage) {
passThrough((AccountMessage) msg);
} else if (msg instanceof APIMessage) {
handleApiMessage((APIMessage) msg);
} else {
handleLocalMessage(msg);
}
}
private void handleLocalMessage(Message msg) {
if (msg instanceof GenerateMessageIdentityCategoryMsg) {
handle((GenerateMessageIdentityCategoryMsg) msg);
} else {
bus.dealWithUnknownMessage(msg);
}
}
@Override
public Map<Class, List<Quota>> getMessageQuotaMap() {
return messageQuotaMap;
}
@Override
public List<Quota> getQuotas() {
return definedQuotas;
}
@Override
@Transactional
public AccountResourceRefInventory changeResourceOwner(String resourceUuid, String newOwnerUuid) {
String sql = "select ref from AccountResourceRefVO ref where ref.resourceUuid = :resUuid";
TypedQuery<AccountResourceRefVO> q = dbf.getEntityManager().createQuery(sql, AccountResourceRefVO.class);
q.setParameter("resUuid", resourceUuid);
List<AccountResourceRefVO> refs = q.getResultList();
if (refs.isEmpty()) {
throw new OperationFailureException(argerr("cannot find the resource[uuid:%s]; wrong resourceUuid or the resource is admin resource",
resourceUuid));
}
AccountResourceRefVO ref = refs.get(0);
final AccountResourceRefInventory origin = AccountResourceRefInventory.valueOf(ref);
for (ResourceOwnerPreChangeExtensionPoint ext : pluginRgty.getExtensionList(ResourceOwnerPreChangeExtensionPoint.class)) {
ext.resourceOwnerPreChange(origin, newOwnerUuid);
}
ref.setAccountUuid(newOwnerUuid);
ref.setOwnerAccountUuid(newOwnerUuid);
ref = dbf.getEntityManager().merge(ref);
CollectionUtils.safeForEach(pluginRgty.getExtensionList(ResourceOwnerAfterChangeExtensionPoint.class),
new ForEachFunction<ResourceOwnerAfterChangeExtensionPoint>() {
@Override
public void run(ResourceOwnerAfterChangeExtensionPoint ext) {
ext.resourceOwnerAfterChange(origin, newOwnerUuid);
}
});
return AccountResourceRefInventory.valueOf(ref);
}
@Override
public void checkApiMessagePermission(APIMessage msg) {
new Auth().check(msg);
}
@Override
public boolean isAdmin(SessionInventory session) {
return AccountConstant.INITIAL_SYSTEM_ADMIN_UUID.equals(session.getAccountUuid());
}
private void handle(GenerateMessageIdentityCategoryMsg msg) {
List<String> adminMsgs = new ArrayList<>();
List<String> userMsgs = new ArrayList<>();
List<Class> apiMsgClasses = BeanUtils.scanClassByType("org.zstack", APIMessage.class);
for (Class clz : apiMsgClasses) {
if (APISearchMessage.class.isAssignableFrom(clz) || APIGetMessage.class.isAssignableFrom(clz)
|| APIListMessage.class.isAssignableFrom(clz)) {
continue;
}
String name = clz.getSimpleName().replaceAll("API", "").replaceAll("Msg", "");
if (clz.isAnnotationPresent(Action.class)) {
userMsgs.add(name);
} else {
adminMsgs.add(name);
}
}
List<String> quotas = new ArrayList<>();
for (List<Quota> quotaList : messageQuotaMap.values()) {
for (Quota q : quotaList) {
for (QuotaPair p : q.getQuotaPairs()) {
quotas.add(String.format("%s %s", p.getName(), p.getValue()));
}
}
}
List<String> as = new ArrayList<>();
for (Map.Entry<Class, MessageAction> e : actions.entrySet()) {
Class api = e.getKey();
MessageAction a = e.getValue();
if (a.adminOnly || a.accountOnly) {
continue;
}
String name = api.getSimpleName().replaceAll("API", "").replaceAll("Msg", "");
StringBuilder sb = new StringBuilder();
sb.append(String.format("%s: ", name));
sb.append(StringUtils.join(a.actions, ", "));
sb.append("\n");
as.add(sb.toString());
}
try {
String folder = PathUtil.join(System.getProperty("user.home"), "zstack-identity");
FileUtils.deleteDirectory(new File(folder));
new File(folder).mkdirs();
String userMsgsPath = PathUtil.join(folder, "non-admin-api.txt");
FileUtils.writeStringToFile(new File(userMsgsPath), StringUtils.join(userMsgs, "\n"));
String adminMsgsPath = PathUtil.join(folder, "admin-api.txt");
FileUtils.writeStringToFile(new File(adminMsgsPath), StringUtils.join(adminMsgs, "\n"));
String quotaPath = PathUtil.join(folder, "quota.txt");
FileUtils.writeStringToFile(new File(quotaPath), StringUtils.join(quotas, "\n"));
String apiIdentityPath = PathUtil.join(folder, "api-identity.txt");
FileUtils.writeStringToFile(new File(apiIdentityPath), StringUtils.join(as, "\n"));
bus.reply(msg, new GenerateMessageIdentityCategoryReply());
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
}
private void passThrough(AccountMessage msg) {
AccountVO vo = dbf.findByUuid(msg.getAccountUuid(), AccountVO.class);
if (vo == null) {
String err = String.format("unable to find account[uuid=%s]", msg.getAccountUuid());
bus.replyErrorByMessageType((Message) msg, errf.instantiateErrorCode(SysErrors.RESOURCE_NOT_FOUND, err));
return;
}
AccountBase base = new AccountBase(vo);
base.handleMessage((Message) msg);
}
private void handleApiMessage(APIMessage msg) {
if (msg instanceof APICreateAccountMsg) {
handle((APICreateAccountMsg) msg);
} else if (msg instanceof APIListAccountMsg) {
handle((APIListAccountMsg) msg);
} else if (msg instanceof APIListUserMsg) {
handle((APIListUserMsg) msg);
} else if (msg instanceof APIListPolicyMsg) {
handle((APIListPolicyMsg) msg);
} else if (msg instanceof APILogInByAccountMsg) {
handle((APILogInByAccountMsg) msg);
} else if (msg instanceof APILogInByUserMsg) {
handle((APILogInByUserMsg) msg);
} else if (msg instanceof APILogOutMsg) {
handle((APILogOutMsg) msg);
} else if (msg instanceof APIValidateSessionMsg) {
handle((APIValidateSessionMsg) msg);
} else if (msg instanceof APICheckApiPermissionMsg) {
handle((APICheckApiPermissionMsg) msg);
} else if (msg instanceof APIGetResourceAccountMsg) {
handle((APIGetResourceAccountMsg) msg);
} else if (msg instanceof APIChangeResourceOwnerMsg) {
handle((APIChangeResourceOwnerMsg) msg);
} else if (msg instanceof APIGetResourceNamesMsg) {
handle((APIGetResourceNamesMsg) msg);
} else {
bus.dealWithUnknownMessage(msg);
}
}
private void handle(APIGetResourceNamesMsg msg) {
List<ResourceInventory> invs = new SQLBatchWithReturn<List<ResourceInventory>>() {
@Override
protected List<ResourceInventory> scripts() {
Query q = dbf.getEntityManager().createNativeQuery("select uuid, resourceName, resourceType from ResourceVO where uuid in (:uuids)");
q.setParameter("uuids", msg.getUuids());
List<Object[]> objs = q.getResultList();
List<ResourceVO> vos = objs.stream().map(ResourceVO::new).collect(Collectors.toList());
return ResourceInventory.valueOf(vos);
}
}.execute();
APIGetResourceNamesReply reply = new APIGetResourceNamesReply();
reply.setInventories(invs);
bus.reply(msg, reply);
}
private void handle(final APIChangeResourceOwnerMsg msg) {
APIChangeResourceOwnerEvent evt = new APIChangeResourceOwnerEvent(msg.getId());
evt.setInventory(changeResourceOwner(msg.getResourceUuid(), msg.getAccountUuid()));
bus.publish(evt);
}
@Transactional(readOnly = true)
private void handle(APIGetResourceAccountMsg msg) {
String sql = "select a, ref.resourceUuid" +
" from AccountResourceRefVO ref, AccountVO a" +
" where a.uuid = ref.accountUuid" +
" and ref.resourceUuid in (:uuids)";
TypedQuery<Tuple> q = dbf.getEntityManager().createQuery(sql, Tuple.class);
q.setParameter("uuids", msg.getResourceUuids());
List<Tuple> tuples = q.getResultList();
Map<String, AccountInventory> ret = new HashMap<>();
for (Tuple t : tuples) {
String resUuid = t.get(1, String.class);
AccountVO vo = t.get(0, AccountVO.class);
ret.put(resUuid, AccountInventory.valueOf(vo));
}
AccountVO admin = dbf.findByUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID, AccountVO.class);
AccountInventory adminInv = AccountInventory.valueOf(admin);
for (String resUuid : msg.getResourceUuids()) {
if (!ret.containsKey(resUuid)) {
ret.put(resUuid, adminInv);
}
}
APIGetResourceAccountReply reply = new APIGetResourceAccountReply();
reply.setInventories(ret);
bus.reply(msg, reply);
}
private void handle(APICheckApiPermissionMsg msg) {
if (msg.getUserUuid() != null) {
SimpleQuery<AccountVO> q = dbf.createQuery(AccountVO.class);
q.add(AccountVO_.uuid, Op.EQ, msg.getSession().getAccountUuid());
q.add(AccountVO_.type, Op.EQ, AccountType.SystemAdmin);
boolean isAdmin = q.isExists();
SimpleQuery<UserVO> uq = dbf.createQuery(UserVO.class);
uq.add(UserVO_.accountUuid, Op.EQ, msg.getSession().getAccountUuid());
uq.add(UserVO_.uuid, Op.EQ, msg.getUserUuid());
boolean isMine = uq.isExists();
if (!isAdmin && !isMine) {
throw new OperationFailureException(operr(
"the user specified by the userUuid[%s] does not belong to the current account, and the" +
" current account is not an admin account, so it has no permission to check the user's" +
"permissions", msg.getUserUuid()
));
}
}
Map<String, String> ret = new HashMap<>();
SessionInventory session = new SessionInventory();
if (msg.getUserUuid() != null) {
UserVO user = dbf.findByUuid(msg.getUserUuid(), UserVO.class);
session.setAccountUuid(user.getAccountUuid());
session.setUserUuid(user.getUuid());
} else {
session = msg.getSession();
}
for (String apiName : msg.getApiNames()) {
try {
Class apiClass = Class.forName(apiName);
APIMessage api = (APIMessage) apiClass.newInstance();
api.setSession(session);
try {
new Auth().check(api);
ret.put(apiName, StatementEffect.Allow.toString());
} catch (ApiMessageInterceptionException e) {
logger.debug(e.getMessage());
ret.put(apiName, StatementEffect.Deny.toString());
}
} catch (ClassNotFoundException e) {
throw new OperationFailureException(argerr("%s is not an API", apiName));
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
}
APICheckApiPermissionReply reply = new APICheckApiPermissionReply();
reply.setInventory(ret);
bus.reply(msg, reply);
}
private void handle(APIValidateSessionMsg msg) {
APIValidateSessionReply reply = new APIValidateSessionReply();
SessionInventory s = sessions.get(msg.getSessionUuid());
Timestamp current = dbf.getCurrentSqlTime();
boolean valid = true;
if (s != null) {
if (current.after(s.getExpiredDate())) {
valid = false;
logOutSession(s.getUuid());
}
} else {
SessionVO session = dbf.findByUuid(msg.getSessionUuid(), SessionVO.class);
if (session != null && current.after(session.getExpiredDate())) {
valid = false;
logOutSession(session.getUuid());
} else if (session == null) {
valid = false;
}
}
reply.setValidSession(valid);
bus.reply(msg, reply);
}
private void handle(APILogOutMsg msg) {
APILogOutReply reply = new APILogOutReply();
logOutSession(msg.getSessionUuid());
bus.reply(msg, reply);
}
private SessionInventory getSession(String accountUuid, String userUuid) {
int maxLoginTimes = org.zstack.identity.IdentityGlobalConfig.MAX_CONCURRENT_SESSION.value(Integer.class);
SimpleQuery<SessionVO> query = dbf.createQuery(SessionVO.class);
query.add(SessionVO_.accountUuid, Op.EQ, accountUuid);
query.add(SessionVO_.userUuid, Op.EQ, userUuid);
long count = query.count();
if (count >= maxLoginTimes) {
String err = String.format("Login sessions hit limit of max allowed concurrent login sessions, max allowed: %s", maxLoginTimes);
throw new BadCredentialsException(err);
}
int sessionTimeout = IdentityGlobalConfig.SESSION_TIMEOUT.value(Integer.class);
SessionVO svo = new SessionVO();
svo.setUuid(Platform.getUuid());
svo.setAccountUuid(accountUuid);
svo.setUserUuid(userUuid);
long expiredTime = getCurrentSqlDate().getTime() + TimeUnit.SECONDS.toMillis(sessionTimeout);
svo.setExpiredDate(new Timestamp(expiredTime));
svo = dbf.persistAndRefresh(svo);
SessionInventory session = SessionInventory.valueOf(svo);
sessions.put(session.getUuid(), session);
return session;
}
private void handle(APILogInByUserMsg msg) {
APILogInReply reply = new APILogInReply();
String accountUuid;
if (msg.getAccountUuid() != null) {
accountUuid = msg.getAccountUuid();
} else {
SimpleQuery<AccountVO> accountq = dbf.createQuery(AccountVO.class);
accountq.select(AccountVO_.uuid);
accountq.add(AccountVO_.name, Op.EQ, msg.getAccountName());
accountUuid = accountq.findValue();
if (accountUuid == null) {
throw new OperationFailureException(argerr("account[%s] not found", msg.getAccountName()));
}
}
SimpleQuery<UserVO> q = dbf.createQuery(UserVO.class);
q.add(UserVO_.accountUuid, Op.EQ, accountUuid);
q.add(UserVO_.password, Op.EQ, msg.getPassword());
q.add(UserVO_.name, Op.EQ, msg.getUserName());
UserVO user = q.find();
if (user == null) {
reply.setError(errf.instantiateErrorCode(IdentityErrors.AUTHENTICATION_ERROR,
"wrong username or password"
));
bus.reply(msg, reply);
return;
}
reply.setInventory(getSession(user.getAccountUuid(), user.getUuid()));
bus.reply(msg, reply);
}
private void handle(APILogInByAccountMsg msg) {
APILogInReply reply = new APILogInReply();
SimpleQuery<AccountVO> q = dbf.createQuery(AccountVO.class);
q.add(AccountVO_.name, Op.EQ, msg.getAccountName());
q.add(AccountVO_.password, Op.EQ, msg.getPassword());
AccountVO vo = q.find();
if (vo == null) {
reply.setError(errf.instantiateErrorCode(IdentityErrors.AUTHENTICATION_ERROR, "wrong account name or password"));
bus.reply(msg, reply);
return;
}
reply.setInventory(getSession(vo.getUuid(), vo.getUuid()));
bus.reply(msg, reply);
}
private void handle(APIListPolicyMsg msg) {
List<PolicyVO> vos = dl.listByApiMessage(msg, PolicyVO.class);
List<PolicyInventory> invs = PolicyInventory.valueOf(vos);
APIListPolicyReply reply = new APIListPolicyReply();
reply.setInventories(invs);
bus.reply(msg, reply);
}
private void handle(APIListUserMsg msg) {
List<UserVO> vos = dl.listByApiMessage(msg, UserVO.class);
List<UserInventory> invs = UserInventory.valueOf(vos);
APIListUserReply reply = new APIListUserReply();
reply.setInventories(invs);
bus.reply(msg, reply);
}
private void handle(APIListAccountMsg msg) {
List<AccountVO> vos = dl.listByApiMessage(msg, AccountVO.class);
List<AccountInventory> invs = AccountInventory.valueOf(vos);
APIListAccountReply reply = new APIListAccountReply();
reply.setInventories(invs);
bus.reply(msg, reply);
}
private void handle(APICreateAccountMsg msg) {
final AccountInventory inv = new SQLBatchWithReturn<AccountInventory>() {
@Override
protected AccountInventory scripts() {
AccountVO vo = new AccountVO();
if (msg.getResourceUuid() != null) {
vo.setUuid(msg.getResourceUuid());
} else {
vo.setUuid(Platform.getUuid());
}
vo.setName(msg.getName());
vo.setDescription(msg.getDescription());
vo.setPassword(msg.getPassword());
vo.setType(msg.getType() != null ? AccountType.valueOf(msg.getType()) : AccountType.Normal);
persist(vo);
reload(vo);
PolicyVO p = new PolicyVO();
p.setUuid(Platform.getUuid());
p.setAccountUuid(vo.getUuid());
p.setName("DEFAULT-READ");
Statement s = new Statement();
s.setName(String.format("read-permission-for-account-%s", vo.getUuid()));
s.setEffect(StatementEffect.Allow);
s.addAction(".*:read");
p.setData(JSONObjectUtil.toJsonString(list(s)));
persist(p);
reload(p);
persist(AccountResourceRefVO.newOwn(vo.getUuid(), p.getUuid(), PolicyVO.class));
p = new PolicyVO();
p.setUuid(Platform.getUuid());
p.setAccountUuid(vo.getUuid());
p.setName("USER-RESET-PASSWORD");
s = new Statement();
s.setName(String.format("user-reset-password-%s", vo.getUuid()));
s.setEffect(StatementEffect.Allow);
s.addAction(String.format("%s:%s", AccountConstant.ACTION_CATEGORY, APIUpdateUserMsg.class.getSimpleName()));
p.setData(JSONObjectUtil.toJsonString(list(s)));
persist(p);
reload(p);
persist(AccountResourceRefVO.newOwn(vo.getUuid(), p.getUuid(), PolicyVO.class));
List<Tuple> ts = Q.New(GlobalConfigVO.class).select(GlobalConfigVO_.name, GlobalConfigVO_.value)
.eq(GlobalConfigVO_.category, AccountConstant.QUOTA_GLOBAL_CONFIG_CATETORY).listTuple();
for (Tuple t : ts) {
String rtype = t.get(0, String.class);
long quota = Long.valueOf(t.get(1, String.class));
QuotaVO qvo = new QuotaVO();
qvo.setUuid(Platform.getUuid());
qvo.setIdentityType(AccountVO.class.getSimpleName());
qvo.setIdentityUuid(vo.getUuid());
qvo.setName(rtype);
qvo.setValue(quota);
persist(qvo);
reload(qvo);
persist(AccountResourceRefVO.newOwn(vo.getUuid(), qvo.getUuid(), QuotaVO.class));
}
reload(vo);
return AccountInventory.valueOf(vo);
}
}.execute();
CollectionUtils.safeForEach(pluginRgty.getExtensionList(AfterCreateAccountExtensionPoint.class),
arg -> arg.afterCreateAccount(inv));
APICreateAccountEvent evt = new APICreateAccountEvent(msg.getId());
evt.setInventory(inv);
bus.publish(evt);
}
@Override
public String getId() {
return bus.makeLocalServiceId(AccountConstant.SERVICE_ID);
}
private void buildResourceTypes() throws ClassNotFoundException {
resourceTypes = new ArrayList<>();
for (String resrouceTypeName : resourceTypeForAccountRef) {
Class<?> rs = Class.forName(resrouceTypeName);
resourceTypes.add(rs);
}
}
private void addResourceType() {
for (AddtionalResourceTypeExtensionPoint ext: pluginRgty.getExtensionList(AddtionalResourceTypeExtensionPoint.class)) {
List<String> list = ext.getAddtionalResourceType();
if (list != null && list.size() > 0) {
resourceTypeForAccountRef.addAll(list);
}
}
}
@Override
public boolean start() {
try {
addResourceType();
buildResourceTypes();
buildActions();
startExpiredSessionCollector();
collectDefaultQuota();
configureGlobalConfig();
setupCanonicalEvents();
for (ReportApiAccountControlExtensionPoint ext : pluginRgty.getExtensionList(ReportApiAccountControlExtensionPoint.class)) {
List<Class> apis = ext.reportApiAccountControl();
DebugUtils.Assert(apis != null, String.format("%s.reportApiAccountControl() returns null", ext.getClass()));
accountApiControlInternal.addAll(apis);
}
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
return true;
}
private void setupCanonicalEvents() {
evtf.on(IdentityCanonicalEvents.ACCOUNT_DELETED_PATH, new EventCallback() {
@Override
public void run(Map tokens, Object data) {
AccountDeletedData d = (AccountDeletedData) data;
SimpleQuery<SessionVO> q = dbf.createQuery(SessionVO.class);
q.select(SessionVO_.uuid);
q.add(SessionVO_.accountUuid, Op.EQ, d.getAccountUuid());
List<String> suuids = q.listValue();
for (String uuid : suuids) {
logOutSession(uuid);
}
if (!suuids.isEmpty()) {
logger.debug(String.format("successfully removed %s sessions for the deleted account[%s]", suuids.size(),
d.getAccountUuid()));
}
}
});
evtf.on(IdentityCanonicalEvents.USER_DELETED_PATH, new EventCallback() {
@Override
public void run(Map tokens, Object data) {
UserDeletedData d = (UserDeletedData) data;
SimpleQuery<SessionVO> q = dbf.createQuery(SessionVO.class);
q.select(SessionVO_.uuid);
q.add(SessionVO_.userUuid, Op.EQ, d.getUserUuid());
List<String> suuids = q.listValue();
for (String uuid : suuids) {
logOutSession(uuid);
}
if (!suuids.isEmpty()) {
logger.debug(String.format("successfully removed %s sessions for the deleted user[%s]", suuids.size(),
d.getUserUuid()));
}
}
});
}
private void configureGlobalConfig() {
String v = IdentityGlobalConfig.ACCOUNT_API_CONTROL.value();
String[] classNames = v.split(",");
for (String cn : classNames) {
cn = cn.trim();
try {
Class clz = Class.forName(cn);
accountApiControl.add(clz);
} catch (ClassNotFoundException e) {
throw new CloudRuntimeException(String.format("no API found for %s", cn));
}
}
IdentityGlobalConfig.ACCOUNT_API_CONTROL.installValidateExtension(new GlobalConfigValidatorExtensionPoint() {
@Override
public void validateGlobalConfig(String category, String name, String oldValue, String newValue) throws GlobalConfigException {
if (newValue.isEmpty()) {
return;
}
String[] classNames = newValue.split(",");
for (String cn : classNames) {
cn = cn.trim();
try {
Class.forName(cn);
} catch (ClassNotFoundException e) {
throw new GlobalConfigException(String.format("no API found for %s", cn));
}
}
}
});
IdentityGlobalConfig.ACCOUNT_API_CONTROL.installUpdateExtension(new GlobalConfigUpdateExtensionPoint() {
@Override
public void updateGlobalConfig(GlobalConfig oldConfig, GlobalConfig newConfig) {
accountApiControl.clear();
if (newConfig.value().isEmpty()) {
return;
}
String[] classNames = newConfig.value().split(",");
for (String name : classNames) {
try {
name = name.trim();
Class clz = Class.forName(name);
accountApiControl.add(clz);
} catch (ClassNotFoundException e) {
throw new CloudRuntimeException(e);
}
}
}
});
}
private void collectDefaultQuota() {
Map<String, Long> defaultQuota = new HashMap<>();
// Add quota and quota checker
for (ReportQuotaExtensionPoint ext : pluginRgty.getExtensionList(ReportQuotaExtensionPoint.class)) {
List<Quota> quotas = ext.reportQuota();
DebugUtils.Assert(quotas != null, String.format("%s.getQuotaPairs() returns null", ext.getClass()));
definedQuotas.addAll(quotas);
for (Quota quota : quotas) {
DebugUtils.Assert(quota.getQuotaPairs() != null,
String.format("%s reports a quota containing a null quotaPairs", ext.getClass()));
for (QuotaPair p : quota.getQuotaPairs()) {
if (defaultQuota.containsKey(p.getName())) {
throw new CloudRuntimeException(String.format("duplicate DefaultQuota[resourceType: %s] reported by %s", p.getName(), ext.getClass()));
}
defaultQuota.put(p.getName(), p.getValue());
nameQuotaMap.put(p.getName(), quota);
}
for (Class clz : quota.getMessagesNeedValidation()) {
if (messageQuotaMap.containsKey(clz)) {
messageQuotaMap.get(clz).add(quota);
} else {
ArrayList<Quota> quotaArrayList = new ArrayList<>();
quotaArrayList.add(quota);
messageQuotaMap.put(clz, quotaArrayList);
}
}
}
}
// Add additional quota checker to quota
for (RegisterQuotaCheckerExtensionPoint ext : pluginRgty.getExtensionList(RegisterQuotaCheckerExtensionPoint.class)) {
// Map<quota name,Set<QuotaValidator>>
Map<String, Set<Quota.QuotaValidator>> m = ext.registerQuotaValidator();
for (Map.Entry<String, Set<Quota.QuotaValidator>> entry : m.entrySet()) {
Quota quota = nameQuotaMap.get(entry.getKey());
quota.addQuotaValidators(entry.getValue());
for (Quota.QuotaValidator q : entry.getValue()) {
for (Class clz : q.getMessagesNeedValidation()) {
if (messageQuotaMap.containsKey(clz)) {
messageQuotaMap.get(clz).add(quota);
} else {
ArrayList<Quota> quotaArrayList = new ArrayList<>();
quotaArrayList.add(quota);
messageQuotaMap.put(clz, quotaArrayList);
}
}
}
}
}
// complete default quota
SimpleQuery<GlobalConfigVO> q = dbf.createQuery(GlobalConfigVO.class);
q.select(GlobalConfigVO_.name);
q.add(GlobalConfigVO_.category, Op.EQ, AccountConstant.QUOTA_GLOBAL_CONFIG_CATETORY);
List<String> existingQuota = q.listValue();
List<GlobalConfigVO> quotaConfigs = new ArrayList<>();
for (Map.Entry<String, Long> e : defaultQuota.entrySet()) {
String rtype = e.getKey();
Long value = e.getValue();
if (existingQuota.contains(rtype)) {
continue;
}
GlobalConfigVO g = new GlobalConfigVO();
g.setCategory(AccountConstant.QUOTA_GLOBAL_CONFIG_CATETORY);
g.setDefaultValue(value.toString());
g.setValue(g.getDefaultValue());
g.setName(rtype);
g.setDescription(String.format("default quota for %s", rtype));
quotaConfigs.add(g);
if (logger.isTraceEnabled()) {
logger.trace(String.format("create default quota[name: %s, value: %s] global config", rtype, value));
}
}
for (GlobalConfigVO vo : quotaConfigs) {
gcf.createGlobalConfig(vo);
}
//
repairAccountQuota(defaultQuota);
}
private void repairAccountQuota(Map<String, Long> defaultQuota) {
SimpleQuery<AccountVO> queryAccounts = dbf.createQuery(AccountVO.class);
queryAccounts.select(AccountVO_.uuid);
queryAccounts.add(AccountVO_.type, Op.EQ, AccountType.Normal);
List<String> normalAccounts = queryAccounts.listValue();
List<QuotaVO> quotas = new ArrayList<>();
for (String nA : normalAccounts) {
SimpleQuery<QuotaVO> queryAccountQuotas = dbf.createQuery(QuotaVO.class);
queryAccountQuotas.select(QuotaVO_.name);
queryAccountQuotas.add(QuotaVO_.identityUuid, Op.EQ, nA);
List<String> existingQuota = queryAccountQuotas.listValue();
for (Map.Entry<String, Long> e : defaultQuota.entrySet()) {
String rtype = e.getKey();
Long value = e.getValue();
if (existingQuota.contains(rtype)) {
continue;
}
QuotaVO q = new QuotaVO();
q.setUuid(Platform.getUuid());
q.setName(rtype);
q.setIdentityUuid(nA);
q.setIdentityType(AccountVO.class.getSimpleName());
q.setValue(value);
quotas.add(q);
if (logger.isTraceEnabled()) {
logger.trace(String.format("create default quota[name: %s, value: %s] global config", rtype, value));
}
}
}
if (!quotas.isEmpty()) {
dbf.persistCollection(quotas);
}
}
private void startExpiredSessionCollector() {
final int interval = IdentityGlobalConfig.SESSION_CLEANUP_INTERVAL.value(Integer.class);
expiredSessionCollector = thdf.submitPeriodicTask(new PeriodicTask() {
@Transactional
private List<String> deleteExpiredSessions() {
String sql = "select s.uuid from SessionVO s where CURRENT_TIMESTAMP >= s.expiredDate";
TypedQuery<String> q = dbf.getEntityManager().createQuery(sql, String.class);
List<String> uuids = q.getResultList();
if (!uuids.isEmpty()) {
String dsql = "delete from SessionVO s where s.uuid in :uuids";
Query dq = dbf.getEntityManager().createQuery(dsql);
dq.setParameter("uuids", uuids);
dq.executeUpdate();
}
return uuids;
}
@Override
public void run() {
List<String> uuids = deleteExpiredSessions();
for (String uuid : uuids) {
sessions.remove(uuid);
}
}
@Override
public TimeUnit getTimeUnit() {
return TimeUnit.SECONDS;
}
@Override
public long getInterval() {
return interval;
}
@Override
public String getName() {
return "ExpiredSessionCleanupThread";
}
});
}
private void buildActions() {
List<Class> apiMsgClasses = BeanUtils.scanClassByType("org.zstack", APIMessage.class);
for (Class clz : apiMsgClasses) {
Action a = (Action) clz.getAnnotation(Action.class);
if (a == null) {
logger.debug(String.format("API message[%s] doesn't have annotation @Action, assume it's an admin only API", clz));
MessageAction ma = new MessageAction();
ma.adminOnly = true;
ma.accountOnly = true;
ma.accountControl = false;
actions.put(clz, ma);
continue;
}
MessageAction ma = new MessageAction();
ma.accountOnly = a.accountOnly();
ma.adminOnly = a.adminOnly();
ma.category = a.category();
ma.actions = new ArrayList<String>();
ma.accountControl = a.accountControl();
ma.accountCheckFields = new ArrayList<AccountCheckField>();
for (String ac : a.names()) {
ma.actions.add(String.format("%s:%s", ma.category, ac));
}
List<Field> allFields = FieldUtils.getAllFields(clz);
for (Field f : allFields) {
APIParam at = f.getAnnotation(APIParam.class);
if (at == null || !at.checkAccount()) {
continue;
}
if (!String.class.isAssignableFrom(f.getType()) && !Collection.class.isAssignableFrom(f.getType())) {
throw new CloudRuntimeException(String.format("@APIParam of %s.%s has checkAccount = true, however," +
" the type of the field is not String or Collection but %s. " +
"This field must be a resource UUID or a collection(e.g. List) of UUIDs",
clz.getName(), f.getName(), f.getType()));
}
AccountCheckField af = new AccountCheckField();
f.setAccessible(true);
af.field = f;
af.param = at;
ma.accountCheckFields.add(af);
}
ma.actions.add(String.format("%s:%s", ma.category, clz.getName()));
ma.actions.add(String.format("%s:%s", ma.category, clz.getSimpleName()));
actions.put(clz, ma);
}
}
@Override
public boolean stop() {
if (expiredSessionCollector != null) {
expiredSessionCollector.cancel(true);
}
return true;
}
@Override
public void prepareDbInitialValue() {
try {
SimpleQuery<AccountVO> q = dbf.createQuery(AccountVO.class);
q.add(AccountVO_.name, Op.EQ, AccountConstant.INITIAL_SYSTEM_ADMIN_NAME);
q.add(AccountVO_.type, Op.EQ, AccountType.SystemAdmin);
if (!q.isExists()) {
AccountVO vo = new AccountVO();
vo.setUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID);
vo.setName(AccountConstant.INITIAL_SYSTEM_ADMIN_NAME);
vo.setPassword(AccountConstant.INITIAL_SYSTEM_ADMIN_PASSWORD);
vo.setType(AccountType.SystemAdmin);
dbf.persist(vo);
logger.debug(String.format("Created initial system admin account[name:%s]", AccountConstant.INITIAL_SYSTEM_ADMIN_NAME));
}
} catch (Exception e) {
throw new CloudRuntimeException("Unable to create default system admin account", e);
}
}
@Override
@Transactional
public void createAccountResourceRef(String accountUuid, String resourceUuid, Class<?> resourceClass) {
if (!resourceTypes.contains(resourceClass)) {
throw new CloudRuntimeException(String.format("%s is not listed in resourceTypeForAccountRef of AccountManager.xml that is spring configuration. you forgot it???", resourceClass.getName()));
}
AccountResourceRefVO ref = AccountResourceRefVO.newOwn(accountUuid, resourceUuid, resourceClass);
dbf.getEntityManager().persist(ref);
}
@Override
public boolean isResourceHavingAccountReference(Class entityClass) {
return resourceTypes.contains(entityClass);
}
@Override
@Transactional(readOnly = true)
public List<String> getResourceUuidsCanAccessByAccount(String accountUuid, Class resourceType) {
String sql = "select a.type from AccountVO a where a.uuid = :auuid";
TypedQuery<AccountType> q = dbf.getEntityManager().createQuery(sql, AccountType.class);
q.setParameter("auuid", accountUuid);
List<AccountType> types = q.getResultList();
if (types.isEmpty()) {
throw new OperationFailureException(argerr("cannot find the account[uuid:%s]", accountUuid));
}
AccountType atype = types.get(0);
if (AccountType.SystemAdmin == atype) {
return null;
}
sql = "select r.resourceUuid from AccountResourceRefVO r where r.accountUuid = :auuid" +
" and r.resourceType = :rtype";
TypedQuery<String> rq = dbf.getEntityManager().createQuery(sql, String.class);
rq.setParameter("auuid", accountUuid);
rq.setParameter("rtype", resourceType.getSimpleName());
List<String> ownResourceUuids = rq.getResultList();
sql = "select r.resourceUuid from SharedResourceVO r where" +
" (r.toPublic = :toPublic or r.receiverAccountUuid = :auuid) and r.resourceType = :rtype";
TypedQuery<String> srq = dbf.getEntityManager().createQuery(sql, String.class);
srq.setParameter("toPublic", true);
srq.setParameter("auuid", accountUuid);
srq.setParameter("rtype", resourceType.getSimpleName());
List<String> shared = srq.getResultList();
shared.addAll(ownResourceUuids);
return shared;
}
@Override
public String getOwnerAccountUuidOfResource(String resourceUuid) {
try {
SimpleQuery<AccountResourceRefVO> q = dbf.createQuery(AccountResourceRefVO.class);
q.select(AccountResourceRefVO_.ownerAccountUuid);
q.add(AccountResourceRefVO_.resourceUuid, Op.EQ, resourceUuid);
String ownerUuid = q.findValue();
DebugUtils.Assert(ownerUuid != null, String.format("cannot find owner uuid for resource[uuid:%s]", resourceUuid));
return ownerUuid;
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
}
@Override
public List<Class> getEntityClassForSoftDeleteEntityExtension() {
return resourceTypes;
}
@Override
@Transactional
public void postSoftDelete(Collection entityIds, Class entityClass) {
String sql = "delete from AccountResourceRefVO ref where ref.resourceUuid in (:uuids) and ref.resourceType = :resourceType";
Query q = dbf.getEntityManager().createQuery(sql);
q.setParameter("uuids", entityIds);
q.setParameter("resourceType", entityClass.getSimpleName());
q.executeUpdate();
}
@Override
public List<Class> getEntityClassForHardDeleteEntityExtension() {
return resourceTypes;
}
@Override
public void postHardDelete(Collection entityIds, Class entityClass) {
if (resourceTypes.contains(entityClass)) {
postSoftDelete(entityIds, entityClass);
}
}
@Override
public List<Class> getMessageClassToIntercept() {
return null;
}
@Override
public InterceptorPosition getPosition() {
return InterceptorPosition.FRONT;
}
private void logOutSession(String sessionUuid) {
SessionInventory session = sessions.get(sessionUuid);
if (session == null) {
SessionVO svo = dbf.findByUuid(sessionUuid, SessionVO.class);
session = svo == null ? null : SessionInventory.valueOf(svo);
}
if (session == null) {
return;
}
final SessionInventory finalSession = session;
CollectionUtils.safeForEach(pluginRgty.getExtensionList(SessionLogoutExtensionPoint.class),
new ForEachFunction<SessionLogoutExtensionPoint>() {
@Override
public void run(SessionLogoutExtensionPoint ext) {
ext.sessionLogout(finalSession);
}
});
sessions.remove(sessionUuid);
dbf.removeByPrimaryKey(sessionUuid, SessionVO.class);
}
@Transactional(readOnly = true)
private Timestamp getCurrentSqlDate() {
Query query = dbf.getEntityManager().createNativeQuery("select current_timestamp()");
return (Timestamp) query.getSingleResult();
}
class Auth {
APIMessage msg;
SessionInventory session;
MessageAction action;
String username;
void validate(APIMessage msg) {
this.msg = msg;
if (msg.getClass().isAnnotationPresent(SuppressCredentialCheck.class)) {
return;
}
action = actions.get(msg.getClass());
sessionCheck();
policyCheck();
msg.setSession(session);
}
void check(APIMessage msg) {
this.msg = msg;
if (msg.getClass().isAnnotationPresent(SuppressCredentialCheck.class)) {
return;
}
DebugUtils.Assert(msg.getSession() != null, "session cannot be null");
session = msg.getSession();
action = actions.get(msg.getClass());
policyCheck();
}
private void accountFieldCheck() throws IllegalAccessException {
Set resourceUuids = new HashSet();
Set operationTargetResourceUuids = new HashSet();
for (AccountCheckField af : action.accountCheckFields) {
Object value = af.field.get(msg);
if (value == null) {
continue;
}
if (String.class.isAssignableFrom(af.field.getType())) {
if (af.param.operationTarget()) {
operationTargetResourceUuids.add(value);
} else {
resourceUuids.add(value);
}
} else if (Collection.class.isAssignableFrom(af.field.getType())) {
if (af.param.operationTarget()) {
operationTargetResourceUuids.addAll((Collection) value);
} else {
resourceUuids.addAll((Collection) value);
}
}
}
if (resourceUuids.isEmpty() && operationTargetResourceUuids.isEmpty()) {
return;
}
// if a resource uuid represents an operation target, it cannot be bypassed by
// the shared resources, as we don't support roles for cross-account sharing.
if (!resourceUuids.isEmpty()) {
SimpleQuery<SharedResourceVO> sq = dbf.createQuery(SharedResourceVO.class);
sq.select(SharedResourceVO_.receiverAccountUuid, SharedResourceVO_.toPublic, SharedResourceVO_.resourceUuid);
sq.add(SharedResourceVO_.resourceUuid, Op.IN, resourceUuids);
List<Tuple> ts = sq.listTuple();
for (Tuple t : ts) {
String ruuid = t.get(0, String.class);
Boolean toPublic = t.get(1, Boolean.class);
String resUuid = t.get(2, String.class);
if (toPublic || session.getAccountUuid().equals(ruuid)) {
// this resource is shared to the account
resourceUuids.remove(resUuid);
}
}
}
resourceUuids.addAll(operationTargetResourceUuids);
if (resourceUuids.isEmpty()) {
return;
}
List<Tuple> ts = SQL.New(
" select avo.name ,arrf.accountUuid ,arrf.resourceUuid ,arrf.resourceType " +
"from AccountResourceRefVO arrf ,AccountVO avo " +
"where arrf.resourceUuid in (:resourceUuids) and avo.uuid = arrf.accountUuid",Tuple.class)
.param("resourceUuids",resourceUuids).list();
for (Tuple t : ts) {
String resourceOwnerName = t.get(0, String.class);
String resourceOwnerAccountUuid = t.get(1, String.class);
String resourceUuid = t.get(2, String.class);
String resourceType = t.get(3, String.class);
if (!session.getAccountUuid().equals(resourceOwnerAccountUuid)) {
throw new ApiMessageInterceptionException(errf.instantiateErrorCode(IdentityErrors.PERMISSION_DENIED,
String.format("operation denied. The resource[uuid: %s, type: %s,ownerAccountName:%s, ownerAccountUuid:%s] doesn't belong to the account[uuid: %s]",
resourceUuid, resourceType, resourceOwnerName, resourceOwnerAccountUuid, session.getAccountUuid())
));
} else {
if (logger.isTraceEnabled()) {
logger.trace(String.format("account-check pass. The resource[uuid: %s, type: %s] belongs to the account[uuid: %s]",
resourceUuid, resourceType, session.getAccountUuid()));
}
}
}
}
private void useDecision(Decision d, boolean userPolicy) {
String policyCategory = userPolicy ? "user policy" : "group policy";
if (d.effect == StatementEffect.Allow) {
logger.debug(String.format("API[name: %s, action: %s] is approved by a %s[name: %s, uuid: %s]," +
" statement[name: %s, action: %s]", msg.getClass().getSimpleName(), d.action,
policyCategory, d.policy.getName(), d.policy.getUuid(), d.statement.getName(), d.actionRule));
} else {
logger.debug(String.format("API[name: %s, action: %s] is denied by a %s[name: %s, uuid: %s]," +
" statement[name: %s, action: %s]", msg.getClass().getSimpleName(), d.action,
policyCategory, d.policy.getName(), d.policy.getUuid(), d.statement.getName(), d.actionRule));
throw new ApiMessageInterceptionException(errf.instantiateErrorCode(IdentityErrors.PERMISSION_DENIED,
String.format("%s denied. user[name: %s, uuid: %s] is denied to execute API[%s]",
policyCategory, username, session.getUuid(), msg.getClass().getSimpleName())
));
}
}
private void policyCheck() {
if (new QuotaUtil().isAdminAccount(session.getAccountUuid())) {
return;
}
if (action.adminOnly) {
throw new ApiMessageInterceptionException(errf.instantiateErrorCode(IdentityErrors.PERMISSION_DENIED,
String.format("API[%s] is admin only", msg.getClass().getSimpleName())));
}
if (action.accountOnly && !session.isAccountSession()) {
throw new ApiMessageInterceptionException(errf.instantiateErrorCode(IdentityErrors.PERMISSION_DENIED,
String.format("API[%s] can only be called by an account, the current session is a user session[user uuid:%s]",
msg.getClass().getSimpleName(), session.getUserUuid())
));
}
if (action.accountCheckFields != null && !action.accountCheckFields.isEmpty()) {
try {
accountFieldCheck();
} catch (ApiMessageInterceptionException ae) {
throw ae;
} catch (Exception e) {
throw new CloudRuntimeException(e);
}
}
if (action.accountControl) {
boolean allow = false;
for (Class clz : accountApiControl) {
if (clz.isAssignableFrom(msg.getClass())) {
allow = true;
break;
}
}
if (!allow) {
for (Class clz : accountApiControlInternal) {
if (clz.isAssignableFrom(msg.getClass())) {
allow = true;
break;
}
}
}
if (!allow) {
throw new ApiMessageInterceptionException(errf.instantiateErrorCode(IdentityErrors.PERMISSION_DENIED,
String.format("the API[%s] is not allowed for normal accounts", msg.getClass())
));
}
}
if (session.isAccountSession()) {
return;
}
SimpleQuery<UserVO> uq = dbf.createQuery(UserVO.class);
uq.select(UserVO_.name);
uq.add(UserVO_.uuid, Op.EQ, session.getUserUuid());
username = uq.findValue();
List<PolicyInventory> userPolicies = getUserPolicies();
Decision d = decide(userPolicies);
if (d != null) {
useDecision(d, true);
return;
}
List<PolicyInventory> groupPolicies = getGroupPolicies();
d = decide(groupPolicies);
if (d != null) {
useDecision(d, false);
return;
}
throw new ApiMessageInterceptionException(errf.instantiateErrorCode(IdentityErrors.PERMISSION_DENIED,
String.format("user[name: %s, uuid: %s] has no policy set for this operation, API[%s] is denied by default. You may either create policies for this user" +
" or add the user into a group with polices set", username, session.getUserUuid(), msg.getClass().getSimpleName())
));
}
@Transactional(readOnly = true)
private List<PolicyInventory> getGroupPolicies() {
String sql = "select p" +
" from PolicyVO p, UserGroupUserRefVO ref, UserGroupPolicyRefVO gref" +
" where p.uuid = gref.policyUuid" +
" and gref.groupUuid = ref.groupUuid" +
" and ref.userUuid = :uuid";
TypedQuery<PolicyVO> q = dbf.getEntityManager().createQuery(sql, PolicyVO.class);
q.setParameter("uuid", session.getUserUuid());
return PolicyInventory.valueOf(q.getResultList());
}
class Decision {
PolicyInventory policy;
String action;
Statement statement;
String actionRule;
StatementEffect effect;
}
private Decision decide(List<PolicyInventory> userPolicies) {
for (String a : action.actions) {
for (PolicyInventory p : userPolicies) {
for (Statement s : p.getStatements()) {
for (String ac : s.getActions()) {
Pattern pattern = Pattern.compile(ac);
Matcher m = pattern.matcher(a);
boolean ret = m.matches();
if (ret) {
Decision d = new Decision();
d.policy = p;
d.action = a;
d.statement = s;
d.actionRule = ac;
d.effect = s.getEffect();
return d;
}
if (logger.isTraceEnabled()) {
logger.trace(String.format("API[name: %s, action: %s] is not matched by policy[name: %s, uuid: %s" +
", statement[name: %s, action: %s, effect: %s]", msg.getClass().getSimpleName(),
a, p.getName(), p.getUuid(), s.getName(), ac, s.getEffect()));
}
}
}
}
}
return null;
}
@Transactional(readOnly = true)
private List<PolicyInventory> getUserPolicies() {
String sql = "select p from PolicyVO p, UserPolicyRefVO ref where ref.userUuid = :uuid and ref.policyUuid = p.uuid";
TypedQuery<PolicyVO> q = dbf.getEntityManager().createQuery(sql, PolicyVO.class);
q.setParameter("uuid", session.getUserUuid());
return PolicyInventory.valueOf(q.getResultList());
}
private void sessionCheck() {
if (msg.getSession() == null) {
throw new ApiMessageInterceptionException(errf.instantiateErrorCode(IdentityErrors.INVALID_SESSION,
String.format("session of message[%s] is null", msg.getMessageName())));
}
if (msg.getSession().getUuid() == null) {
throw new ApiMessageInterceptionException(errf.instantiateErrorCode(IdentityErrors.INVALID_SESSION,
"session uuid is null"));
}
SessionInventory session = sessions.get(msg.getSession().getUuid());
if (session == null) {
SessionVO svo = dbf.findByUuid(msg.getSession().getUuid(), SessionVO.class);
if (svo == null) {
throw new ApiMessageInterceptionException(errf.instantiateErrorCode(IdentityErrors.INVALID_SESSION,
"Session expired"));
}
session = SessionInventory.valueOf(svo);
sessions.put(session.getUuid(), session);
}
Timestamp curr = getCurrentSqlDate();
if (curr.after(session.getExpiredDate())) {
logger.debug(String.format("session expired[%s < %s] for account[uuid:%s]", curr,
session.getExpiredDate(), session.getAccountUuid()));
logOutSession(session.getUuid());
throw new ApiMessageInterceptionException(errf.instantiateErrorCode(IdentityErrors.INVALID_SESSION, "Session expired"));
}
this.session = session;
}
}
@Override
public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionException {
new Auth().validate(msg);
if (msg instanceof APIUpdateAccountMsg) {
validate((APIUpdateAccountMsg) msg);
} else if (msg instanceof APICreatePolicyMsg) {
validate((APICreatePolicyMsg) msg);
} else if (msg instanceof APIAddUserToGroupMsg) {
validate((APIAddUserToGroupMsg) msg);
} else if (msg instanceof APIAttachPolicyToUserGroupMsg) {
validate((APIAttachPolicyToUserGroupMsg) msg);
} else if (msg instanceof APIAttachPolicyToUserMsg) {
validate((APIAttachPolicyToUserMsg) msg);
} else if (msg instanceof APIDetachPolicyFromUserGroupMsg) {
validate((APIDetachPolicyFromUserGroupMsg) msg);
} else if (msg instanceof APIDetachPolicyFromUserMsg) {
validate((APIDetachPolicyFromUserMsg) msg);
} else if (msg instanceof APIShareResourceMsg) {
validate((APIShareResourceMsg) msg);
} else if (msg instanceof APIRevokeResourceSharingMsg) {
validate((APIRevokeResourceSharingMsg) msg);
} else if (msg instanceof APIUpdateUserMsg) {
validate((APIUpdateUserMsg) msg);
} else if (msg instanceof APIDeleteAccountMsg) {
validate((APIDeleteAccountMsg) msg);
} else if (msg instanceof APICreateAccountMsg) {
validate((APICreateAccountMsg) msg);
} else if (msg instanceof APICreateUserMsg) {
validate((APICreateUserMsg) msg);
} else if (msg instanceof APICreateUserGroupMsg) {
validate((APICreateUserGroupMsg) msg);
} else if (msg instanceof APILogInByUserMsg) {
validate((APILogInByUserMsg) msg);
} else if (msg instanceof APIGetAccountQuotaUsageMsg) {
validate((APIGetAccountQuotaUsageMsg) msg);
} else if (msg instanceof APIChangeResourceOwnerMsg) {
validate((APIChangeResourceOwnerMsg) msg);
}
setServiceId(msg);
return msg;
}
private void checkQuotaForChangeResourceOwner(APIChangeResourceOwnerMsg msg) {
String currentAccountUuid = msg.getSession().getAccountUuid();
String resourceTargetOwnerAccountUuid = msg.getAccountUuid();
if (new QuotaUtil().isAdminAccount(resourceTargetOwnerAccountUuid)) {
return;
}
// check if change resource owner to self
SimpleQuery<AccountResourceRefVO> queryAccResRefVO = dbf.createQuery(AccountResourceRefVO.class);
queryAccResRefVO.add(AccountResourceRefVO_.resourceUuid, Op.EQ, msg.getResourceUuid());
AccountResourceRefVO accResRefVO = queryAccResRefVO.find();
String resourceOriginalOwnerAccountUuid = accResRefVO.getOwnerAccountUuid();
if (resourceTargetOwnerAccountUuid.equals(resourceOriginalOwnerAccountUuid)) {
throw new ApiMessageInterceptionException(errf.instantiateErrorCode(IdentityErrors.QUOTA_INVALID_OP,
String.format("Invalid ChangeResourceOwner operation." +
"Original owner is the same as target owner." +
"Current account is [uuid: %s]." +
"The resource target owner account[uuid: %s]." +
"The resource original owner account[uuid:%s].",
currentAccountUuid, resourceTargetOwnerAccountUuid, resourceOriginalOwnerAccountUuid)
));
}
// check quota
Map<String, QuotaPair> pairs = new QuotaUtil().makeQuotaPairs(msg.getAccountUuid());
for (Quota quota : messageQuotaMap.get(APIChangeResourceOwnerMsg.class)) {
quota.getOperator().checkQuota(msg, pairs);
}
}
private void validate(APIChangeResourceOwnerMsg msg) {
checkQuotaForChangeResourceOwner(msg);
}
private void validate(APIGetAccountQuotaUsageMsg msg) {
if (msg.getUuid() == null) {
msg.setUuid(msg.getSession().getAccountUuid());
}
}
private void validate(APILogInByUserMsg msg) {
if (msg.getAccountName() == null && msg.getAccountUuid() == null) {
throw new ApiMessageInterceptionException(argerr(
"accountName and accountUuid cannot both be null, you must specify at least one"
));
}
}
private void validate(APICreateUserGroupMsg msg) {
SimpleQuery<UserGroupVO> q = dbf.createQuery(UserGroupVO.class);
q.add(UserGroupVO_.accountUuid, Op.EQ, msg.getAccountUuid());
q.add(UserGroupVO_.name, Op.EQ, msg.getName());
if (q.isExists()) {
throw new ApiMessageInterceptionException(argerr("unable to create a group. A group called %s is already under the account[uuid:%s]",
msg.getName(), msg.getAccountUuid()));
}
}
private void validate(APICreateUserMsg msg) {
SimpleQuery<UserVO> q = dbf.createQuery(UserVO.class);
q.add(UserVO_.accountUuid, Op.EQ, msg.getAccountUuid());
q.add(UserVO_.name, Op.EQ, msg.getName());
if (q.isExists()) {
throw new ApiMessageInterceptionException(argerr("unable to create a user. A user called %s is already under the account[uuid:%s]",
msg.getName(), msg.getAccountUuid()));
}
}
private void validate(APICreateAccountMsg msg) {
SimpleQuery<AccountVO> q = dbf.createQuery(AccountVO.class);
q.add(AccountVO_.name, Op.EQ, msg.getName());
if (q.isExists()) {
throw new ApiMessageInterceptionException(argerr("unable to create an account. An account already called %s", msg.getName()));
}
}
private void validate(APIDeleteAccountMsg msg) {
if (new QuotaUtil().isAdminAccount(msg.getUuid())) {
throw new ApiMessageInterceptionException(argerr(
"unable to delete an account. The account is an admin account"
));
}
}
private void validate(APIUpdateUserMsg msg) {
if (msg.getUuid() == null && msg.getSession().isAccountSession()) {
throw new ApiMessageInterceptionException(argerr(
"the current session is an account session. You need to specify the field 'uuid' of the user" +
" you want to update"
));
}
if (msg.getSession().isAccountSession()) {
return;
}
if (msg.getUuid() != null && !msg.getSession().getUserUuid().equals(msg.getUuid())) {
throw new ApiMessageInterceptionException(argerr("your are login as a user, you cannot another user[uuid:%s]", msg.getUuid()));
}
msg.setUuid(msg.getSession().getUserUuid());
}
private void validate(APIRevokeResourceSharingMsg msg) {
if (!msg.isAll() && (msg.getAccountUuids() == null || msg.getAccountUuids().isEmpty())) {
throw new ApiMessageInterceptionException(argerr(
"all is set to false, accountUuids cannot be null or empty"
));
}
}
private void validate(APIShareResourceMsg msg) {
if (!msg.isToPublic() && (msg.getAccountUuids() == null || msg.getAccountUuids().isEmpty())) {
throw new ApiMessageInterceptionException(argerr(
"toPublic is set to false, accountUuids cannot be null or empty"
));
}
}
private void validate(APIDetachPolicyFromUserMsg msg) {
PolicyVO policy = dbf.findByUuid(msg.getPolicyUuid(), PolicyVO.class);
UserVO user = dbf.findByUuid(msg.getUserUuid(), UserVO.class);
if (!policy.getAccountUuid().equals(msg.getAccountUuid())) {
throw new ApiMessageInterceptionException(argerr("policy[name: %s, uuid: %s] doesn't belong to the account[uuid: %s]",
policy.getName(), policy.getUuid(), msg.getSession().getAccountUuid()));
}
if (!user.getAccountUuid().equals(msg.getAccountUuid())) {
throw new ApiMessageInterceptionException(argerr("user[name: %s, uuid: %s] doesn't belong to the account[uuid: %s]",
user.getName(), user.getUuid(), msg.getSession().getAccountUuid()));
}
}
private void validate(APIDetachPolicyFromUserGroupMsg msg) {
PolicyVO policy = dbf.findByUuid(msg.getPolicyUuid(), PolicyVO.class);
UserGroupVO group = dbf.findByUuid(msg.getGroupUuid(), UserGroupVO.class);
if (!policy.getAccountUuid().equals(msg.getAccountUuid())) {
throw new ApiMessageInterceptionException(argerr("policy[name: %s, uuid: %s] doesn't belong to the account[uuid: %s]",
policy.getName(), policy.getUuid(), msg.getSession().getAccountUuid()));
}
if (!group.getAccountUuid().equals(msg.getAccountUuid())) {
throw new ApiMessageInterceptionException(argerr("group[name: %s, uuid: %s] doesn't belong to the account[uuid: %s]",
group.getName(), group.getUuid(), msg.getSession().getAccountUuid()));
}
}
private void validate(APIAttachPolicyToUserMsg msg) {
PolicyVO policy = dbf.findByUuid(msg.getPolicyUuid(), PolicyVO.class);
UserVO user = dbf.findByUuid(msg.getUserUuid(), UserVO.class);
if (!policy.getAccountUuid().equals(msg.getAccountUuid())) {
throw new ApiMessageInterceptionException(argerr("policy[name: %s, uuid: %s] doesn't belong to the account[uuid: %s]",
policy.getName(), policy.getUuid(), msg.getSession().getAccountUuid()));
}
if (!user.getAccountUuid().equals(msg.getAccountUuid())) {
throw new ApiMessageInterceptionException(argerr("user[name: %s, uuid: %s] doesn't belong to the account[uuid: %s]",
user.getName(), user.getUuid(), msg.getSession().getAccountUuid()));
}
}
private void validate(APIAttachPolicyToUserGroupMsg msg) {
PolicyVO policy = dbf.findByUuid(msg.getPolicyUuid(), PolicyVO.class);
UserGroupVO group = dbf.findByUuid(msg.getGroupUuid(), UserGroupVO.class);
if (!policy.getAccountUuid().equals(msg.getAccountUuid())) {
throw new ApiMessageInterceptionException(argerr("policy[name: %s, uuid: %s] doesn't belong to the account[uuid: %s]",
policy.getName(), policy.getUuid(), msg.getSession().getAccountUuid()));
}
if (!group.getAccountUuid().equals(msg.getAccountUuid())) {
throw new ApiMessageInterceptionException(argerr("group[name: %s, uuid: %s] doesn't belong to the account[uuid: %s]",
group.getName(), group.getUuid(), msg.getSession().getAccountUuid()));
}
}
private void validate(APIAddUserToGroupMsg msg) {
UserVO user = dbf.findByUuid(msg.getUserUuid(), UserVO.class);
UserGroupVO group = dbf.findByUuid(msg.getGroupUuid(), UserGroupVO.class);
if (!user.getAccountUuid().equals(msg.getAccountUuid())) {
throw new ApiMessageInterceptionException(argerr("user[name: %s, uuid: %s] doesn't belong to the account[uuid: %s]",
user.getName(), user.getUuid(), msg.getSession().getAccountUuid()));
}
if (!group.getAccountUuid().equals(msg.getSession().getAccountUuid())) {
throw new ApiMessageInterceptionException(argerr("group[name: %s, uuid: %s] doesn't belong to the account[uuid: %s]",
group.getName(), group.getUuid(), msg.getSession().getAccountUuid()));
}
}
private void validate(APICreatePolicyMsg msg) {
for (Statement s : msg.getStatements()) {
if (s.getEffect() == null) {
throw new ApiMessageInterceptionException(argerr("a statement must have effect field. Invalid statement[%s]", JSONObjectUtil.toJsonString(s)));
}
if (s.getActions() == null) {
throw new ApiMessageInterceptionException(argerr("a statement must have action field. Invalid statement[%s]", JSONObjectUtil.toJsonString(s)));
}
if (s.getActions().isEmpty()) {
throw new ApiMessageInterceptionException(argerr("a statement must have a non-empty action field. Invalid statement[%s]",
JSONObjectUtil.toJsonString(s)));
}
}
}
private void validate(APIUpdateAccountMsg msg) {
AccountVO a = dbf.findByUuid(msg.getSession().getAccountUuid(), AccountVO.class);
if (msg.getUuid() == null) {
msg.setUuid(msg.getSession().getAccountUuid());
}
if (a.getType() == AccountType.SystemAdmin) {
if (msg.getName() != null && (msg.getUuid() == null || msg.getUuid().equals(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID))) {
throw new OperationFailureException(operr(
"the name of admin account cannot be updated"
));
}
return;
}
AccountVO account = dbf.findByUuid(msg.getUuid(), AccountVO.class);
if (!account.getUuid().equals(a.getUuid())) {
throw new OperationFailureException(operr("account[uuid: %s, name: %s] is a normal account, it cannot reset the password of another account[uuid: %s]",
account.getUuid(), account.getName(), msg.getUuid()));
}
}
private void setServiceId(APIMessage msg) {
if (msg instanceof AccountMessage) {
AccountMessage amsg = (AccountMessage) msg;
bus.makeTargetServiceIdByResourceUuid(msg, AccountConstant.SERVICE_ID, amsg.getAccountUuid());
}
}
public void setResourceTypeForAccountRef(List<String> resourceTypeForAccountRef) {
this.resourceTypeForAccountRef = resourceTypeForAccountRef;
}
}