Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@

### Features Added

* Added Deny List for Targeting Filter.

```yml
feature-management:
TargetingTest:
enabled-for:
-
name: Microsoft.Targeting
parameters:
users:
- Jeff
- Alicia
groups:
-
name: Ring0
rolloutPercentage: 100
-
name: Ring1
rolloutPercentage: 100
defaultRolloutPercentage: 50
exclusion:
users:
- Ross
```

### Breaking Changes

### Bugs Fixed
Expand Down
7 changes: 6 additions & 1 deletion sdk/spring/spring-cloud-azure-feature-management/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ feature-management:

### TargetingFilter

This filter provides the capability to enable a feature for a target audience. An in-depth explanation of targeting is explained in the targeting section below. The filter parameters include an audience object which describes users, groups, and a default percentage of the user base that should have access to the feature. Each group object that is listed in the target audience must also specify what percentage of the group's members should have access. If a user is specified in the users section directly, or if the user is in the included percentage of any of the group rollouts, or if the user falls into the default rollout percentage then that user will have the feature enabled.
This filter provides the capability to enable a feature for a target audience. An in-depth explanation of targeting is explained in the targeting section below. The filter parameters include an audience object which describes users, groups, and a default percentage of the user base that should have access to the feature, and an exclusion object for users and groups that should never be targeted. Each group object that is listed in the target audience must also specify what percentage of the group's members should have access. If a user is specified in the users section directly, or if the user is in the included percentage of any of the group rollouts, or if the user falls into the default rollout percentage then that user will have the feature enabled.

```yml
feature-management:
Expand All @@ -219,6 +219,11 @@ feature-management:
name: Ring1
rolloutPercentage: 100
defaultRolloutPercentage: 50
exclusion:
users:
- Ross
groups:
- Ring2
```

## Targeting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -49,6 +50,11 @@ public class TargetingFilter implements FeatureFilter {
*/
protected static final String AUDIENCE = "Audience";

/**
* Audience that always returns false
*/
protected static final String EXCLUSION = "exclusion";

/**
* Error message for when the total Audience value is greater than 100 percent.
*/
Expand Down Expand Up @@ -111,40 +117,52 @@ public boolean evaluate(FeatureFilterEvaluationContext context) {

Map<String, Object> parameters = context.getParameters();

if (parameters != null) {
Object audienceObject = parameters.get(AUDIENCE);
if (audienceObject != null) {
parameters = (Map<String, Object>) audienceObject;
}
Object audienceObject = parameters.get(AUDIENCE);
if (audienceObject != null) {
parameters = (Map<String, Object>) audienceObject;
}

updateValueFromMapToList(parameters, USERS);
updateValueFromMapToList(parameters, GROUPS);
updateValueFromMapToList(parameters, USERS);
updateValueFromMapToList(parameters, GROUPS);

settings.setAudience(OBJECT_MAPPER.convertValue(parameters, Audience.class));
Map<String, Map<String, Object>> exclusionMap = (Map<String, Map<String, Object>>) parameters
.get(EXCLUSION);
Map<String, Object> exclusionAsLists = new HashMap<>();
if (exclusionMap != null) {
exclusionAsLists.put(USERS, exclusionMap.getOrDefault(USERS, new HashMap<String, Object>()).values());
exclusionAsLists.put(GROUPS, exclusionMap.getOrDefault(GROUPS, new HashMap<String, Object>()).values());
}
parameters.put(EXCLUSION, exclusionAsLists);
settings.setAudience(OBJECT_MAPPER.convertValue(parameters, Audience.class));

validateSettings(settings);

Audience audience = settings.getAudience();

if (targetingContext.getUserId() != null
&& audience.getUsers() != null
&& audience.getUsers().stream()
.anyMatch(user -> equals(targetingContext.getUserId(), user))) {
return true;
// Need to Check denied first
if (targetUser(targetingContext.getUserId(), audience.getExclusion().getUsers())) {
return false;
}

if (targetingContext.getGroups() != null && audience.getGroups() != null) {
if (targetingContext.getGroups() != null && audience.getExclusion().getGroups() != null) {
for (String group : targetingContext.getGroups()) {
Optional<GroupRollout> groupRollout = audience.getGroups().stream()
.filter(g -> equals(g.getName(), group)).findFirst();

Optional<String> groupRollout = audience.getExclusion().getGroups().stream()
.filter(g -> equals(g, group)).findFirst();
if (groupRollout.isPresent()) {
String audienceContextId = targetingContext.getUserId() + "\n" + context.getName() + "\n" + group;
return false;
}
}
}

if (isTargeted(audienceContextId, groupRollout.get().getRolloutPercentage())) {
return true;
}
// Check if Allowed
if (targetUser(targetingContext.getUserId(), audience.getUsers())) {
return true;
}

if (targetingContext.getGroups() != null && audience.getGroups() != null) {
for (String group : targetingContext.getGroups()) {
if (targetGroup(audience, targetingContext, context, group)) {
return true;
}
}
}
Expand All @@ -153,6 +171,25 @@ public boolean evaluate(FeatureFilterEvaluationContext context) {

return isTargeted(defaultContextId, settings.getAudience().getDefaultRolloutPercentage());
}

private boolean targetUser(String userId, List<String> users) {
return userId != null && users != null && users.stream().anyMatch(user -> equals(userId, user));
}

private boolean targetGroup(Audience audience, TargetingFilterContext targetingContext,
FeatureFilterEvaluationContext context, String group) {
Optional<GroupRollout> groupRollout = audience.getGroups().stream()
.filter(g -> equals(g.getName(), group)).findFirst();

if (groupRollout.isPresent()) {
String audienceContextId = targetingContext.getUserId() + "\n" + context.getName() + "\n" + group;

if (isTargeted(audienceContextId, groupRollout.get().getRolloutPercentage())) {
return true;
}
}
return false;
}

private boolean validateTargetingContext(TargetingFilterContext targetingContext) {
boolean hasUserDefined = StringUtils.hasText(targetingContext.getUserId());
Expand Down Expand Up @@ -213,8 +250,7 @@ void validateSettings(TargetingFilterSettings settings) {
}

Audience audience = settings.getAudience();
if (audience.getDefaultRolloutPercentage() < 0
|| audience.getDefaultRolloutPercentage() > 100) {
if (audience.getDefaultRolloutPercentage() < 0 || audience.getDefaultRolloutPercentage() > 100) {
paramName = AUDIENCE + "." + audience.getDefaultRolloutPercentage();
reason = OUT_OF_RANGE;

Expand Down Expand Up @@ -259,7 +295,7 @@ private boolean equals(String s1, String s2) {
private void updateValueFromMapToList(Map<String, Object> parameters, String key) {
Object objectMap = parameters.get(key);
if (objectMap instanceof Map) {
List<Object> toType = ((Map<String, Object>) objectMap).values().stream().collect(Collectors.toList());
Collection<Object> toType = ((Map<String, Object>) objectMap).values();
parameters.put(key, toType);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

/**
* Audience of a TargetingFilter rollout
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class Audience {

private List<String> users;
Expand All @@ -15,6 +18,8 @@ public class Audience {

private double defaultRolloutPercentage;

private Exclusion exclusion = new Exclusion();

/**
* @return the users
*/
Expand Down Expand Up @@ -57,4 +62,18 @@ public void setDefaultRolloutPercentage(double defaultRolloutPercentage) {
this.defaultRolloutPercentage = defaultRolloutPercentage;
}

/**
* @return the exclusion
*/
public Exclusion getExclusion() {
return exclusion;
}

/**
* @param exclusion the exclusion to set
*/
public void setExclusion(Exclusion exclusion) {
this.exclusion = exclusion;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.spring.cloud.feature.management.implementation.targeting;

import java.util.ArrayList;
import java.util.List;

/**
* Deny list of a TargetingFilter rollout
*/
public class Exclusion {

private List<String> users = new ArrayList<>();

private List<String> groups = new ArrayList<>();

/**
* @return the users
*/
public List<String> getUsers() {
return users;
}

/**
* @param users the users to set
*/
public void setUsers(List<String> users) {
this.users = users;
}

/**
* @return the groups
*/
public List<String> getGroups() {
return groups;
}

/**
* @param groups the audiences to set
*/
public void setGroups(List<String> groups) {
this.groups = groups;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.feature.management.models;

import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
Expand Down Expand Up @@ -38,7 +39,11 @@ public void setName(String name) {
* @return the parameters
*/
public Map<String, Object> getParameters() {
return parameters;
Map<String, Object> params = new HashMap<String, Object>();
if (parameters != null) {
params.putAll(parameters);
}
return params;
}

/**
Expand Down
Loading