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
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 All @@ -74,6 +80,7 @@ public class TargetingFilter implements FeatureFilter {

/**
* Filter for targeting a user/group/percentage of users.
*
* @param contextAccessor Accessor for identifying the current user/group when evaluating
*/
public TargetingFilter(TargetingContextAccessor contextAccessor) {
Expand All @@ -83,6 +90,7 @@ public TargetingFilter(TargetingContextAccessor contextAccessor) {

/**
* `Microsoft.TargetingFilter` evaluates a user/group/overall rollout of a feature.
*
* @param contextAccessor Context for evaluating the users/groups.
* @param options enables customization of the filter.
*/
Expand Down Expand Up @@ -111,40 +119,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 @@ -154,6 +174,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());
boolean hasGroupsDefined = targetingContext.getGroups() != null;
Expand All @@ -168,6 +207,7 @@ private boolean validateTargetingContext(TargetingFilterContext targetingContext

/**
* Computes the percentage that the contextId falls into.
*
* @param contextId Id of the context being targeted
* @return the bucket value of the context id
* @throws TargetingException Unable to create hash of target context
Expand Down Expand Up @@ -198,6 +238,7 @@ private boolean isTargeted(String contextId, double percentage) {

/**
* Validates the settings of a targeting filter.
*
* @param settings targeting filter settings
* @throws TargetingException when a required parameter is missing or percentage value is greater than 100.
*/
Expand All @@ -213,8 +254,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 All @@ -237,6 +277,7 @@ void validateSettings(TargetingFilterSettings settings) {

/**
* Checks if two strings are equal, ignores case if configured to.
*
* @param s1 string to compare
* @param s2 string to compare
* @return true if the strings are equal
Expand All @@ -251,6 +292,7 @@ private boolean equals(String s1, String s2) {
/**
* Looks at the given key in the parameters and coverts it to a list if it is currently a map. Used for updating
* fields in the targeting filter.
*
* @param <T> Type of object inside of parameters for the given key
* @param parameters map of generic objects
* @param key key of object int the parameters map
Expand All @@ -259,7 +301,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