From 117fd5ea3b4a50d3910626daa5841755a14ddf29 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 12 Apr 2023 10:04:57 -0700 Subject: [PATCH 1/8] Adding Exclusion --- .../management/filters/TargetingFilter.java | 50 +++++++++++++------ .../implementation/targeting/Audience.java | 16 ++++++ .../implementation/targeting/Exclusion.java | 44 ++++++++++++++++ 3 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Exclusion.java diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java index eeb6807743cd..547a88793642 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java @@ -59,8 +59,8 @@ public class TargetingFilter implements FeatureFilter { /** * Object Mapper for converting configurations to features */ - protected static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder() - .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build(); + protected static final ObjectMapper OBJECT_MAPPER = + JsonMapper.builder().configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build(); /** * Accessor for identifying the current user/group when evaluating @@ -74,6 +74,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) { @@ -83,6 +84,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. */ @@ -126,18 +128,30 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { validateSettings(settings); Audience audience = settings.getAudience(); - - if (targetingContext.getUserId() != null - && audience.getUsers() != null - && audience.getUsers().stream() - .anyMatch(user -> equals(targetingContext.getUserId(), user))) { + + // Need to Check Denyed first + if (targetUser(targetingContext.getUserId(), audience.getExclusion().getUsers())) { + return false; + } + + if (targetingContext.getGroups() != null && audience.getExclusion().getGroups() != null) { + for (String group : targetingContext.getGroups()) { + Optional groupRollout = audience.getExclusion().getGroups().stream().filter(g -> equals(g, group)).findFirst(); + if (groupRollout.isPresent()) { + return false; + } + } + } + + // Check if Allowed + if (targetUser(targetingContext.getUserId(), audience.getUsers())) { return true; } if (targetingContext.getGroups() != null && audience.getGroups() != null) { for (String group : targetingContext.getGroups()) { - Optional groupRollout = audience.getGroups().stream() - .filter(g -> equals(g.getName(), group)).findFirst(); + Optional groupRollout = + audience.getGroups().stream().filter(g -> equals(g.getName(), group)).findFirst(); if (groupRollout.isPresent()) { String audienceContextId = targetingContext.getUserId() + "\n" + context.getName() + "\n" + group; @@ -154,6 +168,10 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { return isTargeted(defaultContextId, settings.getAudience().getDefaultRolloutPercentage()); } + private boolean targetUser(String userId, List users) { + return userId != null && users != null && users.stream().anyMatch(user -> equals(userId, user)); + } + private boolean validateTargetingContext(TargetingFilterContext targetingContext) { boolean hasUserDefined = StringUtils.hasText(targetingContext.getUserId()); boolean hasGroupsDefined = targetingContext.getGroups() != null; @@ -168,6 +186,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 @@ -198,8 +217,10 @@ 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. + * @throws TargetingException when a required parameter is missing or percentage value is greater + * than 100. */ void validateSettings(TargetingFilterSettings settings) { String paramName = ""; @@ -213,8 +234,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; @@ -237,6 +257,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 @@ -249,8 +270,9 @@ 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. + * 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 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 diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Audience.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Audience.java index bf0d4c7be236..e92e8dc8d6fb 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Audience.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Audience.java @@ -15,6 +15,8 @@ public class Audience { private double defaultRolloutPercentage; + private Exclusion exclusion; + /** * @return the users */ @@ -57,4 +59,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; + } + } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Exclusion.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Exclusion.java new file mode 100644 index 000000000000..ea5ff1802b54 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Exclusion.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.implementation.targeting; + +import java.util.List; + +/** + * Deny list of a TargetingFilter rollout + */ +public class Exclusion { + + private List users; + + private List groups; + + /** + * @return the users + */ + public List getUsers() { + return users; + } + + /** + * @param users the users to set + */ + public void setUsers(List users) { + this.users = users; + } + + /** + * @return the groups + */ + public List getGroups() { + return groups; + } + + /** + * @param groups the audiences to set + */ + public void setGroups(List groups) { + this.groups = groups; + } + +} From 4860e1070d975abec05e7684a9bed8952ca407c8 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 12 Apr 2023 15:31:15 -0700 Subject: [PATCH 2/8] Deny List --- .../README.md | 7 +- .../management/filters/TargetingFilter.java | 56 ++++++++----- .../implementation/targeting/Audience.java | 2 +- .../implementation/targeting/Exclusion.java | 5 +- .../FeatureFilterEvaluationContext.java | 7 +- .../filters/TargetingFilterTest.java | 84 +++++++++++++++++-- 6 files changed, 126 insertions(+), 35 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management/README.md b/sdk/spring/spring-cloud-azure-feature-management/README.md index d8020de01359..127001dd28d2 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/README.md +++ b/sdk/spring/spring-cloud-azure-feature-management/README.md @@ -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: @@ -219,6 +219,11 @@ feature-management: name: Ring1 rolloutPercentage: 100 defaultRolloutPercentage: 50 + exclusion: + users: + - Ross + groups: + - Ring2 ``` ## Targeting diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java index 547a88793642..858b60df728f 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java @@ -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; @@ -48,6 +49,11 @@ public class TargetingFilter implements FeatureFilter { * Audience in the filter */ protected static final String AUDIENCE = "Audience"; + + /* + * + */ + protected static final String EXCLUSION = "exclusion"; /** * Error message for when the total Audience value is greater than 100 percent. @@ -59,8 +65,8 @@ public class TargetingFilter implements FeatureFilter { /** * Object Mapper for converting configurations to features */ - protected static final ObjectMapper OBJECT_MAPPER = - JsonMapper.builder().configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build(); + protected static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build(); /** * Accessor for identifying the current user/group when evaluating @@ -113,36 +119,43 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { Map parameters = context.getParameters(); - if (parameters != null) { - Object audienceObject = parameters.get(AUDIENCE); - if (audienceObject != null) { - parameters = (Map) audienceObject; - } + Object audienceObject = parameters.get(AUDIENCE); + if (audienceObject != null) { + parameters = (Map) audienceObject; + } - updateValueFromMapToList(parameters, USERS); - updateValueFromMapToList(parameters, GROUPS); + updateValueFromMapToList(parameters, USERS); + updateValueFromMapToList(parameters, GROUPS); - settings.setAudience(OBJECT_MAPPER.convertValue(parameters, Audience.class)); + Map> exclusionMap = (Map>) parameters + .get(EXCLUSION); + Map exclusionAsLists = new HashMap<>(); + if (exclusionMap != null) { + exclusionAsLists.put(USERS, exclusionMap.getOrDefault(USERS, new HashMap()).values()); + exclusionAsLists.put(GROUPS, exclusionMap.getOrDefault(GROUPS, new HashMap()).values()); } + parameters.put(EXCLUSION, exclusionAsLists); + settings.setAudience(OBJECT_MAPPER.convertValue(parameters, Audience.class)); validateSettings(settings); Audience audience = settings.getAudience(); - + // Need to Check Denyed first if (targetUser(targetingContext.getUserId(), audience.getExclusion().getUsers())) { return false; } - + if (targetingContext.getGroups() != null && audience.getExclusion().getGroups() != null) { for (String group : targetingContext.getGroups()) { - Optional groupRollout = audience.getExclusion().getGroups().stream().filter(g -> equals(g, group)).findFirst(); + Optional groupRollout = audience.getExclusion().getGroups().stream() + .filter(g -> equals(g, group)).findFirst(); if (groupRollout.isPresent()) { return false; } } } - + // Check if Allowed if (targetUser(targetingContext.getUserId(), audience.getUsers())) { return true; @@ -150,8 +163,8 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { if (targetingContext.getGroups() != null && audience.getGroups() != null) { for (String group : targetingContext.getGroups()) { - Optional groupRollout = - audience.getGroups().stream().filter(g -> equals(g.getName(), group)).findFirst(); + Optional groupRollout = audience.getGroups().stream() + .filter(g -> equals(g.getName(), group)).findFirst(); if (groupRollout.isPresent()) { String audienceContextId = targetingContext.getUserId() + "\n" + context.getName() + "\n" + group; @@ -219,8 +232,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. + * @throws TargetingException when a required parameter is missing or percentage value is greater than 100. */ void validateSettings(TargetingFilterSettings settings) { String paramName = ""; @@ -270,8 +282,8 @@ 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. + * 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 Type of object inside of parameters for the given key * @param parameters map of generic objects @@ -281,7 +293,7 @@ private boolean equals(String s1, String s2) { private void updateValueFromMapToList(Map parameters, String key) { Object objectMap = parameters.get(key); if (objectMap instanceof Map) { - List toType = ((Map) objectMap).values().stream().collect(Collectors.toList()); + Collection toType = ((Map) objectMap).values(); parameters.put(key, toType); } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Audience.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Audience.java index e92e8dc8d6fb..eda612013829 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Audience.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Audience.java @@ -15,7 +15,7 @@ public class Audience { private double defaultRolloutPercentage; - private Exclusion exclusion; + private Exclusion exclusion = new Exclusion(); /** * @return the users diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Exclusion.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Exclusion.java index ea5ff1802b54..331fe636f2b1 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Exclusion.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Exclusion.java @@ -2,6 +2,7 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.implementation.targeting; +import java.util.ArrayList; import java.util.List; /** @@ -9,9 +10,9 @@ */ public class Exclusion { - private List users; + private List users = new ArrayList<>(); - private List groups; + private List groups = new ArrayList<>(); /** * @return the users diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureFilterEvaluationContext.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureFilterEvaluationContext.java index 91c4e8d3cae2..623785e621b0 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureFilterEvaluationContext.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureFilterEvaluationContext.java @@ -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; @@ -38,7 +39,11 @@ public void setName(String name) { * @return the parameters */ public Map getParameters() { - return parameters; + Map params = new HashMap(); + if (parameters != null) { + params.putAll(parameters); + } + return params; } /** diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java index b6d000136853..0acad22e1027 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java @@ -21,7 +21,7 @@ import com.azure.spring.cloud.feature.management.targeting.TargetingContextAccessor; import com.azure.spring.cloud.feature.management.targeting.TargetingEvaluationOptions; -@SpringBootTest(classes = {TestConfiguration.class, SpringBootTest.class}) +@SpringBootTest(classes = { TestConfiguration.class, SpringBootTest.class }) public class TargetingFilterTest { private static final String USERS = "users"; @@ -32,8 +32,6 @@ public class TargetingFilterTest { private static final String DEFAULT_ROLLOUT_PERCENTAGE = "defaultRolloutPercentage"; - private static final String REQUIRED_PARAMETER = "Value cannot be null."; - private static final String OUT_OF_RANGE = "The value is out of the accepted range."; @Test @@ -301,22 +299,92 @@ public void targetedNull() { FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); - Map parameters = new LinkedHashMap(); + Map parameters = new LinkedHashMap<>(); - Map users = new LinkedHashMap(); + Map users = new LinkedHashMap<>(); users.put("0", "Doe"); parameters.put(USERS, users); - parameters.put(GROUPS, new LinkedHashMap()); + parameters.put(GROUPS, new LinkedHashMap<>()); parameters.put(DEFAULT_ROLLOUT_PERCENTAGE, 0); context.setParameters(null); context.setFeatureName("testFeature"); TargetingFilter filter = new TargetingFilter(new TargetingFilterTestContextAccessor("doe", null), options); + assertFalse(filter.evaluate(context)); + } - Exception exception = assertThrows(TargetingException.class, () -> filter.evaluate(context)); - assertEquals("Audience : " + REQUIRED_PARAMETER, exception.getMessage()); + @Test + public void excludeUser() { + FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); + + Map parameters = new LinkedHashMap<>(); + + Map users = new LinkedHashMap<>(); + users.put("0", "Doe"); + + parameters.put(USERS, users); + parameters.put(GROUPS, new LinkedHashMap<>()); + parameters.put(DEFAULT_ROLLOUT_PERCENTAGE, 0); + + context.setParameters(parameters); + context.setFeatureName("testFeature"); + + TargetingFilter filter = new TargetingFilter(new TargetingFilterTestContextAccessor("Doe", null)); + + assertTrue(filter.evaluate(context)); + + // Now the users is excluded + Map excludes = new LinkedHashMap<>(); + Map excludedUsers = new LinkedHashMap<>(); + excludedUsers.put("0", "Doe"); + + excludes.put(USERS, excludedUsers); + parameters.put("exclusion", excludes); + + context.setParameters(parameters); + + assertFalse(filter.evaluate(context)); + } + + @Test + public void excludeGroup() { + FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); + + Map parameters = new LinkedHashMap(); + + Map groups = new LinkedHashMap(); + Map g1 = new LinkedHashMap(); + g1.put("name", "g1"); + g1.put("rolloutPercentage", "100"); + groups.put("0", g1); + + parameters.put(USERS, new LinkedHashMap()); + parameters.put(GROUPS, groups); + parameters.put(DEFAULT_ROLLOUT_PERCENTAGE, 0); + + context.setParameters(parameters); + context.setFeatureName("testFeature"); + + ArrayList targetedGroups = new ArrayList(); + targetedGroups.add("g1"); + + TargetingFilter filter = new TargetingFilter(new TargetingFilterTestContextAccessor(null, targetedGroups)); + + assertTrue(filter.evaluate(context)); + + // Now the users is excluded + Map excludes = new LinkedHashMap<>(); + Map excludedGroups = new LinkedHashMap<>(); + excludedGroups.put("0", "g1"); + + excludes.put(GROUPS, excludedGroups); + parameters.put("exclusion", excludes); + + context.setParameters(parameters); + + assertFalse(filter.evaluate(context)); } class TargetingFilterTestContextAccessor implements TargetingContextAccessor { From 4a6e2648fb57735360c3a76caa55e8c47ce84699 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 12 Apr 2023 15:37:49 -0700 Subject: [PATCH 3/8] Updating Change Log --- .../CHANGELOG.md | 27 +++++++++++++++++++ .../implementation/targeting/Audience.java | 3 +++ 2 files changed, 30 insertions(+) diff --git a/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md b/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md index 614a11d3ebe7..80cc5d0145b9 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md @@ -4,6 +4,33 @@ ### 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 + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Audience.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Audience.java index eda612013829..517f2a21de57 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Audience.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/targeting/Audience.java @@ -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 users; From 0c8fed7acca683ce42b94169b66d93d3501e113d Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 12 Apr 2023 15:38:41 -0700 Subject: [PATCH 4/8] Update CHANGELOG.md --- sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md b/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md index 80cc5d0145b9..6f809e5a7e74 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md @@ -31,8 +31,6 @@ feature-management: ### Breaking Changes -### Breaking Changes - ### Bugs Fixed ### Other Changes From 731c2d69d355f4e22a8be2ecfa41c2d16e8ea150 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 12 Apr 2023 15:40:20 -0700 Subject: [PATCH 5/8] Missing Comment --- .../cloud/feature/management/filters/TargetingFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java index 858b60df728f..f0c28a9d0fc2 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java @@ -51,7 +51,7 @@ public class TargetingFilter implements FeatureFilter { protected static final String AUDIENCE = "Audience"; /* - * + * Audience that always returns false */ protected static final String EXCLUSION = "exclusion"; From bfaa1a2c836312d34bcf28d45b350d54c6223957 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 13 Apr 2023 09:58:21 -0700 Subject: [PATCH 6/8] Update TargetingFilter.java --- .../cloud/feature/management/filters/TargetingFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java index f0c28a9d0fc2..0aad3a0df8ac 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java @@ -49,7 +49,7 @@ public class TargetingFilter implements FeatureFilter { * Audience in the filter */ protected static final String AUDIENCE = "Audience"; - + /* * Audience that always returns false */ @@ -141,7 +141,7 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { Audience audience = settings.getAudience(); - // Need to Check Denyed first + // Need to Check denied first if (targetUser(targetingContext.getUserId(), audience.getExclusion().getUsers())) { return false; } From 7b1c46184b1c5af8fec81f3943c726dc8de9973f Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 13 Apr 2023 10:29:03 -0700 Subject: [PATCH 7/8] Update TargetingFilter.java --- .../cloud/feature/management/filters/TargetingFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java index 0aad3a0df8ac..49f79e042353 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java @@ -50,7 +50,7 @@ public class TargetingFilter implements FeatureFilter { */ protected static final String AUDIENCE = "Audience"; - /* + /** * Audience that always returns false */ protected static final String EXCLUSION = "exclusion"; From 05b8df17212169be0bdd0d4c9781cc10ef49ca82 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 18 May 2023 10:10:17 -0700 Subject: [PATCH 8/8] Making targetGroup into a method --- .../management/filters/TargetingFilter.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java index 49f79e042353..f11fbceef1ee 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java @@ -163,15 +163,8 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { if (targetingContext.getGroups() != null && audience.getGroups() != null) { for (String group : targetingContext.getGroups()) { - Optional 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; - } + if (targetGroup(audience, targetingContext, context, group)) { + return true; } } } @@ -185,6 +178,21 @@ private boolean targetUser(String userId, List 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 = 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;