Skip to content

Commit

Permalink
fix(papi): handle expression language for dynamic limit in plan rate-…
Browse files Browse the repository at this point in the history
…limit policies
  • Loading branch information
jourdiw committed Sep 20, 2024
1 parent 5c18740 commit 77e4706
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright © 2015 The Gravitee team (http://gravitee.io)
*
* Licensed 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 io.gravitee.rest.api.model.api;

import io.gravitee.common.util.TemplatedValueHashMap;
import io.gravitee.definition.model.DefinitionVersion;
import io.gravitee.rest.api.model.v4.api.GenericApiEntity;
import java.util.Map;
import java.util.stream.Collectors;

/**
* Serialization of an API to be used with the TemplateEngine
* An equivalent to ApiProperties and ApiVariables classes in the Gateway.
*/
public class ApiTemplateVariables {

private final GenericApiEntity api;

public ApiTemplateVariables(final GenericApiEntity api) {
this.api = api;
}

public String getId() {
return this.api.getId();
}

public String getName() {
return this.api.getName();
}

public String getVersion() {
return this.api.getApiVersion();
}

public Map<String, String> getProperties() {
if (this.api != null) {
if (DefinitionVersion.V4.equals(this.api.getDefinitionVersion())) {
var v4Api = (io.gravitee.rest.api.model.v4.api.ApiEntity) api;
if (v4Api.getProperties() != null) {
return v4Api
.getProperties()
.stream()
.collect(
Collectors.toMap(
io.gravitee.definition.model.v4.property.Property::getKey,
io.gravitee.definition.model.v4.property.Property::getValue,
(v1, v2) -> {
throw new IllegalArgumentException(String.format("Duplicate key for values %s and %s", v1, v2));
},
TemplatedValueHashMap::new
)
);
}
} else {
var v2Api = (io.gravitee.rest.api.model.api.ApiEntity) api;
if (v2Api.getProperties() != null) {
return v2Api.getProperties().getValues();
}
}
}

return Map.of();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
import io.gravitee.definition.jackson.datatype.GraviteeMapper;
import io.gravitee.definition.model.flow.Flow;
import io.gravitee.definition.model.v4.flow.step.Step;
import io.gravitee.el.TemplateEngine;
import io.gravitee.rest.api.model.PlanEntity;
import io.gravitee.rest.api.model.api.ApiTemplateVariables;
import io.gravitee.rest.api.model.v4.api.GenericApiEntity;
import io.gravitee.rest.api.model.v4.plan.GenericPlanEntity;
import io.gravitee.rest.api.model.v4.plan.PlanSecurityType;
import io.gravitee.rest.api.portal.rest.model.PeriodTimeUnit;
Expand All @@ -37,12 +40,14 @@
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
* @author Florent CHAMFROY (florent.chamfroy at graviteesource.com)
* @author GraviteeSource Team
*/
@Slf4j
@Component
public class PlanMapper {

Expand All @@ -55,11 +60,7 @@ public class PlanMapper {
private final String QUOTA_POLICY_ID = "quota";
private final Map<String, String> POLICY_CONFIGURATION_ATTRIBUTES = Map.of(QUOTA_POLICY_ID, "quota", RATE_LIMIT_POLICY_ID, "rate");

public Plan convert(PlanEntity plan) {
return convert((GenericPlanEntity) plan);
}

public Plan convert(GenericPlanEntity plan) {
public Plan convert(GenericPlanEntity plan, GenericApiEntity api) {
final Plan planItem = new Plan();

planItem.setCharacteristics(plan.getCharacteristics());
Expand All @@ -77,15 +78,15 @@ public Plan convert(GenericPlanEntity plan) {
planItem.setGeneralConditions(plan.getGeneralConditions());
planItem.setMode(PlanMode.valueOf(plan.getPlanMode().name()));

planItem.setUsageConfiguration(this.toUsageConfiguration(plan));
planItem.setUsageConfiguration(this.toUsageConfiguration(plan, api));

return planItem;
}

private PlanUsageConfiguration toUsageConfiguration(GenericPlanEntity plan) {
private PlanUsageConfiguration toUsageConfiguration(GenericPlanEntity plan, GenericApiEntity api) {
var configuration = new PlanUsageConfiguration();
configuration.setRateLimit(this.getMostRestrictivePolicyConfiguration(RATE_LIMIT_POLICY_ID, plan));
configuration.setQuota(this.getMostRestrictivePolicyConfiguration(QUOTA_POLICY_ID, plan));
configuration.setRateLimit(this.getMostRestrictivePolicyConfiguration(RATE_LIMIT_POLICY_ID, plan, api));
configuration.setQuota(this.getMostRestrictivePolicyConfiguration(QUOTA_POLICY_ID, plan, api));
return configuration;
}

Expand All @@ -96,7 +97,7 @@ private PlanUsageConfiguration toUsageConfiguration(GenericPlanEntity plan) {
* @param plan -- GenericPlanEntity
* @return A TimePeriodConfiguration with the longest duration between calls. Null if no valid configuration exists.
*/
private TimePeriodConfiguration getMostRestrictivePolicyConfiguration(String policyId, GenericPlanEntity plan) {
private TimePeriodConfiguration getMostRestrictivePolicyConfiguration(String policyId, GenericPlanEntity plan, GenericApiEntity api) {
List<String> policyConfigurations = this.getEnabledConfigurationsByPolicyId(policyId, plan);

// Configuration with the longest minimum duration between calls
Expand All @@ -119,8 +120,13 @@ private TimePeriodConfiguration getMostRestrictivePolicyConfiguration(String pol
return;
}

var limitToUse = this.getLimitToUse(configuration, api);
if (limitToUse < 0) {
return;
}

var configAsTimePeriodConfiguration = new TimePeriodConfiguration();
configAsTimePeriodConfiguration.setLimit(this.getLimitToUse(configuration));
configAsTimePeriodConfiguration.setLimit(limitToUse);
configAsTimePeriodConfiguration.setPeriodTime(configuration.get(POLICY_CONFIGURATION_PERIOD_TIME).intValue());
configAsTimePeriodConfiguration.setPeriodTimeUnit(
PeriodTimeUnit.valueOf(configuration.get(POLICY_CONFIGURATION_PERIOD_TIME_UNIT).asText())
Expand Down Expand Up @@ -158,15 +164,37 @@ private Duration calculateDuration(TimePeriodConfiguration configuration) {
.dividedBy(configuration.getLimit().intValue());
}

private long getLimitToUse(JsonNode configuration) {
private long getLimitToUse(JsonNode configuration, GenericApiEntity api) {
// Dynamic limit exists and limit is either missing or equal to 0
if (
configuration.has(POLICY_CONFIGURATION_DYNAMIC_LIMIT) &&
(!configuration.has(POLICY_CONFIGURATION_LIMIT) || configuration.get(POLICY_CONFIGURATION_LIMIT).intValue() == 0)
) {
return Long.parseLong(configuration.get(POLICY_CONFIGURATION_DYNAMIC_LIMIT).asText());
return parseLimit(configuration.get(POLICY_CONFIGURATION_DYNAMIC_LIMIT), api);
}
return configuration.get(POLICY_CONFIGURATION_LIMIT).longValue();
return parseLimit(configuration.get(POLICY_CONFIGURATION_LIMIT), api);
}

private long parseLimit(JsonNode limitNode, GenericApiEntity api) {
var evaluatedLimit = evaluateLimit(limitNode, api);
if (evaluatedLimit != null) {
try {
return Long.parseLong(evaluatedLimit);
} catch (NumberFormatException ignored) {
log.debug("Limit could not be parsed: {}", evaluatedLimit);
}
}
return -1L;
}

private String evaluateLimit(JsonNode limitNode, GenericApiEntity api) {
var apiParams = new ApiTemplateVariables(api);

TemplateEngine templateEngine = TemplateEngine.templateEngine();
templateEngine.getTemplateContext().setVariable("api", apiParams);
templateEngine.getTemplateContext().setVariable("properties", apiParams.getProperties());

return templateEngine.eval(limitNode.asText(), String.class).onErrorReturnItem(limitNode.asText()).blockingGet();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public Response getApiPlansByApiId(@PathParam("apiId") String apiId, @BeanParam
.filter(plan -> PlanStatus.PUBLISHED.equals(plan.getPlanStatus()))
.filter(plan -> groupService.isUserAuthorizedToAccessApiData(genericApiEntity, plan.getExcludedGroups(), username))
.sorted(Comparator.comparingInt(GenericPlanEntity::getOrder))
.map(p -> planMapper.convert(p))
.map(p -> planMapper.convert(p, genericApiEntity))
.collect(Collectors.toList());

return createListResponse(executionContext, plans, paginationParam);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public Response getApiByApiId(@PathParam("apiId") String apiId, @QueryParam("inc
.filter(plan -> PlanStatus.PUBLISHED.equals(plan.getPlanStatus()))
.filter(plan -> groupService.isUserAuthorizedToAccessApiData(genericApiEntity, plan.getExcludedGroups(), username))
.sorted(Comparator.comparingInt(GenericPlanEntity::getOrder))
.map(p -> planMapper.convert(p))
.map(p -> planMapper.convert(p, genericApiEntity))
.collect(Collectors.toList());
api.setPlans(plans);
}
Expand Down
Loading

0 comments on commit 77e4706

Please sign in to comment.