blob: e35a332c54d130950455be071d9ae3e6e57e2f0c [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.ratelimit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
import org.apache.cloudstack.acl.APIChecker;
import org.apache.cloudstack.api.command.admin.ratelimit.ResetApiLimitCmd;
import org.apache.cloudstack.api.command.user.ratelimit.GetApiLimitCmd;
import org.apache.cloudstack.api.response.ApiLimitResponse;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import com.cloud.configuration.Config;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.RequestLimitException;
import com.cloud.user.Account;
import com.cloud.user.AccountService;
import com.cloud.user.User;
import com.cloud.utils.component.AdapterBase;
@Component
public class ApiRateLimitServiceImpl extends AdapterBase implements APIChecker, ApiRateLimitService {
private static final Logger s_logger = Logger.getLogger(ApiRateLimitServiceImpl.class);
/**
* True if api rate limiting is enabled
*/
private boolean enabled = false;
/**
* Fixed time duration where api rate limit is set, in seconds
*/
private int timeToLive = 1;
/**
* Max number of api requests during timeToLive duration.
*/
private int maxAllowed = 30;
private LimitStore _store = null;
@Inject
AccountService _accountService;
@Inject
ConfigurationDao _configDao;
@Override
public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
super.configure(name, params);
if (_store == null) {
// get global configured duration and max values
String isEnabled = _configDao.getValue(Config.ApiLimitEnabled.key());
if (isEnabled != null) {
enabled = Boolean.parseBoolean(isEnabled);
}
String duration = _configDao.getValue(Config.ApiLimitInterval.key());
if (duration != null) {
timeToLive = Integer.parseInt(duration);
}
String maxReqs = _configDao.getValue(Config.ApiLimitMax.key());
if (maxReqs != null) {
maxAllowed = Integer.parseInt(maxReqs);
}
// create limit store
EhcacheLimitStore cacheStore = new EhcacheLimitStore();
int maxElements = 10000;
String cachesize = _configDao.getValue(Config.ApiLimitCacheSize.key());
if (cachesize != null) {
maxElements = Integer.parseInt(cachesize);
}
CacheManager cm = CacheManager.create();
Cache cache = new Cache("api-limit-cache", maxElements, false, false, timeToLive, timeToLive);
cm.addCache(cache);
s_logger.info("Limit Cache created with timeToLive=" + timeToLive + ", maxAllowed=" + maxAllowed + ", maxElements=" + maxElements);
cacheStore.setCache(cache);
_store = cacheStore;
}
return true;
}
@Override
public ApiLimitResponse searchApiLimit(Account caller) {
ApiLimitResponse response = new ApiLimitResponse();
response.setAccountId(caller.getUuid());
response.setAccountName(caller.getAccountName());
StoreEntry entry = _store.get(caller.getId());
if (entry == null) {
/* Populate the entry, thus unlocking any underlying mutex */
entry = _store.create(caller.getId(), timeToLive);
response.setApiIssued(0);
response.setApiAllowed(maxAllowed);
response.setExpireAfter(timeToLive);
} else {
response.setApiIssued(entry.getCounter());
response.setApiAllowed(maxAllowed - entry.getCounter());
response.setExpireAfter(entry.getExpireDuration());
}
return response;
}
@Override
public boolean resetApiLimit(Long accountId) {
if (accountId != null) {
_store.create(accountId, timeToLive);
} else {
_store.resetCounters();
}
return true;
}
@Override
public boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException {
// check if api rate limiting is enabled or not
if (!enabled) {
return true;
}
Long accountId = user.getAccountId();
Account account = _accountService.getAccount(accountId);
if (_accountService.isRootAdmin(account.getId())) {
// no API throttling on root admin
return true;
}
StoreEntry entry = _store.get(accountId);
if (entry == null) {
/* Populate the entry, thus unlocking any underlying mutex */
entry = _store.create(accountId, timeToLive);
}
/* Increment the client count and see whether we have hit the maximum allowed clients yet. */
int current = entry.incrementAndGet();
if (current <= maxAllowed) {
s_logger.trace("account (" + account.getAccountId() + "," + account.getAccountName() + ") has current count = " + current);
return true;
} else {
long expireAfter = entry.getExpireDuration();
// for this exception, we can just show the same message to user and admin users.
String msg = "The given user has reached his/her account api limit, please retry after " + expireAfter + " ms.";
s_logger.warn(msg);
throw new RequestLimitException(msg);
}
}
@Override
public List<Class<?>> getCommands() {
List<Class<?>> cmdList = new ArrayList<Class<?>>();
cmdList.add(ResetApiLimitCmd.class);
cmdList.add(GetApiLimitCmd.class);
return cmdList;
}
@Override
public void setTimeToLive(int timeToLive) {
this.timeToLive = timeToLive;
}
@Override
public void setMaxAllowed(int max) {
maxAllowed = max;
}
@Override
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}