This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new 8eeb89357b3 Handle SAML claims without a NameFormat and combined claim
annotations (#3685)
8eeb89357b3 is described below
commit 8eeb89357b31dab2899a245c27279ef7b1c7e922
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri Nov 6 06:27:23 2026 +0000
Handle SAML claims without a NameFormat and combined claim annotations
(#3685)
---
.../interceptor/ClaimsAuthorizingInterceptor.java | 34 ++++++++----
.../ClaimsAuthorizingInterceptorTest.java | 61 ++++++++++++++++++++++
2 files changed, 85 insertions(+), 10 deletions(-)
diff --git
a/rt/security/src/main/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptor.java
b/rt/security/src/main/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptor.java
index 2c52512a6ed..4236388a6dc 100644
---
a/rt/security/src/main/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptor.java
+++
b/rt/security/src/main/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptor.java
@@ -53,6 +53,9 @@ public class ClaimsAuthorizingInterceptor extends
AbstractPhaseInterceptor<Messa
private static final Set<String> SKIP_METHODS = new HashSet<>(
Arrays.asList("wait", "notify", "notifyAll", "equals", "toString",
"hashCode"));
+ // The SAML 2.0 default for an Attribute without a NameFormat, and the
@Claim default format
+ private static final String UNSPECIFIED_NAME_FORMAT =
+ "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified";
// Claims configured explicitly via setClaims, keyed by method name
private Map<String, List<ClaimBean>> claims = new HashMap<>();
@@ -157,16 +160,23 @@ public class ClaimsAuthorizingInterceptor extends
AbstractPhaseInterceptor<Messa
org.apache.cxf.rt.security.claims.ClaimCollection actualClaims =
sc.getClaims();
for (ClaimBean claimBean : list) {
+ String claimType = claimBean.getClaim().getClaimType();
org.apache.cxf.rt.security.claims.Claim matchingClaim = null;
for (org.apache.cxf.rt.security.claims.Claim cl : actualClaims) {
+ if (claimType == null) {
+ break;
+ }
if (cl instanceof SAMLClaim) {
- // If it's a SAMLClaim the name + nameformat must match
what's configured
- if
(((SAMLClaim)cl).getName().equals(claimBean.getClaim().getClaimType())
- &&
((SAMLClaim)cl).getNameFormat().equals(claimBean.getClaimFormat())) {
+ // If it's a SAMLClaim the name + nameformat must match
what's configured. The
+ // NameFormat is optional, in which case it is the
unspecified name format
+ SAMLClaim samlClaim = (SAMLClaim)cl;
+ if (claimType.equals(samlClaim.getName())
+ && getNameFormat(claimBean.getClaimFormat())
+ .equals(getNameFormat(samlClaim.getNameFormat())))
{
matchingClaim = cl;
break;
}
- } else if
(cl.getClaimType().equals(claimBean.getClaim().getClaimType())) {
+ } else if (claimType.equals(cl.getClaimType())) {
matchingClaim = cl;
break;
}
@@ -256,6 +266,10 @@ public class ClaimsAuthorizingInterceptor extends
AbstractPhaseInterceptor<Messa
}
}
+ private static String getNameFormat(String nameFormat) {
+ return nameFormat == null ? UNSPECIFIED_NAME_FORMAT : nameFormat;
+ }
+
private static boolean isClaimOverridden(ClaimBean bean, List<ClaimBean>
mClaims) {
for (ClaimBean methodBean : mClaims) {
if
(bean.getClaim().getClaimType().equals(methodBean.getClaim().getClaimType())
@@ -270,13 +284,13 @@ public class ClaimsAuthorizingInterceptor extends
AbstractPhaseInterceptor<Messa
Claims claimsAnn, Claim claimAnn) {
List<ClaimBean> claimsList = new ArrayList<>();
- final List<Claim> annClaims;
+ // Both @Claims and @Claim may be present, in which case all their
claims apply
+ final List<Claim> annClaims = new ArrayList<>();
if (claimsAnn != null) {
- annClaims = Arrays.asList(claimsAnn.value());
- } else if (claimAnn != null) {
- annClaims = Collections.singletonList(claimAnn);
- } else {
- annClaims = Collections.emptyList();
+ annClaims.addAll(Arrays.asList(claimsAnn.value()));
+ }
+ if (claimAnn != null) {
+ annClaims.add(claimAnn);
}
for (Claim ann : annClaims) {
org.apache.cxf.rt.security.claims.Claim claim = new
org.apache.cxf.rt.security.claims.Claim();
diff --git
a/rt/security/src/test/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptorTest.java
b/rt/security/src/test/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptorTest.java
index e0da1ca8352..09c66236acc 100644
---
a/rt/security/src/test/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptorTest.java
+++
b/rt/security/src/test/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptorTest.java
@@ -536,6 +536,43 @@ public class ClaimsAuthorizingInterceptorTest {
assertDenied(in, prepareMessage(method, createDefaultClaim("user")));
}
+ @Test
+ public void testClaimWithoutNameFormat() throws Exception {
+ // A SAML attribute without a NameFormat has the unspecified name
format, the default
+ ClaimsAuthorizingInterceptor in = createInterceptor(new
NameFormatService());
+ Method method = NameFormatService.class.getMethod("defaultFormat");
+
+ in.handleMessage(prepareMessage(method,
+ createClaim(SAMLClaim.SAML_ROLE_ATTRIBUTENAME_DEFAULT, null,
"admin")));
+ assertDenied(in, prepareMessage(method,
+ createClaim(SAMLClaim.SAML_ROLE_ATTRIBUTENAME_DEFAULT, null,
"user")));
+ }
+
+ @Test
+ public void testClaimWithoutNameFormatDoesNotMatchOtherFormat() throws
Exception {
+ ClaimsAuthorizingInterceptor in = createInterceptor(new
NameFormatService());
+ Method method = NameFormatService.class.getMethod("specificFormat");
+
+ assertDenied(in, prepareMessage(method,
createClaim("http://cxf/roles", null, "admin")));
+ in.handleMessage(prepareMessage(method,
createClaim("http://cxf/roles", "http://claims", "admin")));
+ }
+
+ @Test
+ public void testClaimAndClaimsAnnotationsAreCombined() throws Exception {
+ ClaimsAuthorizingInterceptor in = createInterceptor(new
CombinedClaimsService());
+ Method method = CombinedClaimsService.class.getMethod("update");
+ org.apache.cxf.rt.security.claims.Claim location =
createClaim("http://location",
+ "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified", "UK");
+
+ in.handleMessage(prepareMessage(method, createDefaultClaim("admin"),
location));
+ assertDenied(in, prepareMessage(method, createDefaultClaim("admin")));
+ assertDenied(in, prepareMessage(method, location));
+
+ Method classMethod = CombinedClaimsService.class.getMethod("list");
+ in.handleMessage(prepareMessage(classMethod,
createDefaultClaim("user"), location));
+ assertDenied(in, prepareMessage(classMethod,
createDefaultClaim("user")));
+ }
+
private static ClaimsAuthorizingInterceptor createInterceptor(Object
service) {
ClaimsAuthorizingInterceptor in = new ClaimsAuthorizingInterceptor();
in.setSecuredObject(service);
@@ -756,6 +793,30 @@ public class ClaimsAuthorizingInterceptorTest {
@Claim(name = "http://location", value = "UK")
public static class LocationInheritingService extends BaseService {
}
+ public static class NameFormatService {
+ @Claim("admin")
+ public void defaultFormat() {
+
+ }
+
+ @Claim(name = "http://cxf/roles", format = "http://claims", value =
"admin")
+ public void specificFormat() {
+
+ }
+ }
+ @Claims({@Claim(name = "http://location", value = "UK") })
+ @Claim("user")
+ public static class CombinedClaimsService {
+ @Claims({@Claim(name = "http://location", value = "UK") })
+ @Claim("admin")
+ public void update() {
+
+ }
+
+ public void list() {
+
+ }
+ }
public static class OverridingService extends BaseService {
@Override
@Claim("admin")