[java-plugin-shibd-oidc] branch main updated: WIP: Add OIDC claims extraction and agent response

Codeberg noreply at shibboleth.net
Fri Nov 21 14:23:32 UTC 2025


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch main
in repository java-plugin-shibd-oidc.

View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-oidc/commit/282d0eeb3ee4ae071c9ddbcfdbd3ffdb58840444

The following commit(s) were added to refs/heads/main by this push:
     new 282d0ee  WIP: Add OIDC claims extraction and agent response
282d0ee is described below

commit 282d0eeb3ee4ae071c9ddbcfdbd3ffdb58840444
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Nov 21 14:23:20 2025 +0000

    WIP: Add OIDC claims extraction and agent response
    
     - Need to add attribute resolver to claims extraction
     - Need to verify the claims extraction logic.
---
 .../idp/flows/sp/consumer/oidc/oidc-beans.xml      |   4 +
 .../idp/flows/sp/consumer/oidc/oidc-flow.xml       |   2 +
 .../sp/oidc/profile/impl/ExtractOIDCClaims.java    | 387 +++++++++++++++++++++
 .../sp/oidc/profile/impl/PrepareAgentResponse.java |  35 ++
 .../sp/oidc/profile/impl/ProcessEndUserClaims.java |   5 +-
 5 files changed, 430 insertions(+), 3 deletions(-)

diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
index 07fb82c..f9dff82 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
@@ -550,4 +550,8 @@
         </property>
     </bean>
     
+    <bean id="ExtractOIDCClaims" class="net.shibboleth.sp.oidc.profile.impl.ExtractOIDCClaims" scope="prototype"/>
+    
+    <bean id="PrepareAgentResponse" class="net.shibboleth.sp.oidc.profile.impl.PrepareAgentResponse" scope="prototype"/>
+    
 </beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
index afd13c1..6f1e973 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
@@ -99,6 +99,8 @@
    <!--     <evaluate expression="ValidateOIDCAuthentication" /> -->
    <!-- <evaluate expression="PopulateSubjectCanonicalizationContext" />
         <evaluate expression="WriteAuditLog" /> -->
+        <evaluate expression="ExtractOIDCClaims"/>
+        <evaluate expression="PrepareAgentResponse"/>
         <evaluate expression="'proceed'" />        
         <transition on="proceed" to="proceed" />   
     </action-state>
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExtractOIDCClaims.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExtractOIDCClaims.java
new file mode 100644
index 0000000..0caf134
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExtractOIDCClaims.java
@@ -0,0 +1,387 @@
+/*
+ * 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 net.shibboleth.sp.oidc.profile.impl;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.context.navigate.RecursiveTypedParentContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.metadata.resolver.MetadataResolver;
+import org.slf4j.Logger;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.minidev.json.JSONObject;
+import net.shibboleth.idp.attribute.AttributeDecodingException;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeSupport;
+import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.shibboleth.idp.attribute.filter.AttributeFilter;
+import net.shibboleth.idp.attribute.filter.AttributeFilterException;
+import net.shibboleth.idp.attribute.filter.context.AttributeFilterContext;
+import net.shibboleth.idp.attribute.filter.context.AttributeFilterContext.Direction;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoder;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry;
+import net.shibboleth.idp.attribute.transcoding.TranscoderSupport;
+import net.shibboleth.idp.attribute.transcoding.TranscodingRule;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.profile.config.OIDCSSORelyingPartyConfiguration;
+import net.shibboleth.oidc.profile.context.EndUserClaimsContext;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
+import net.shibboleth.profile.context.navigate.RelyingPartyIdLookupFunction;
+import net.shibboleth.saml.profile.context.navigate.SAMLMetadataContextLookupFunction;
+import net.shibboleth.shared.annotation.constraint.Live;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.service.ServiceException;
+import net.shibboleth.shared.service.ServiceableComponent;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+
+/**
+ * An action that extracts OIDC End-User claims from the combined id_token and UserInfo response claims in the 
+ * {@link EndUserClaimsContext}.
+ */
+public class ExtractOIDCClaims extends AbstractApplicationAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractOIDCClaims.class);
+    
+    /** Strategy used to look up {@link EndUserClaimsContext} to operate on. */
+    @Nonnull private Function<ProfileRequestContext,EndUserClaimsContext> endUserClaimsContextLookupStrategy;
+    
+    /** Strategy used to look up a {@link RelyingPartyContext} for configuration options. */
+    @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+    
+    /** Strategy used to create {@link AttributeContext} to hold results. */
+    @Nonnull private Function<ProfileRequestContext,AttributeContext> attributeContextCreationStrategy;
+    
+    /** Context containing the claims(s) to process. */
+    @NonnullBeforeExec private EndUserClaimsContext endUserClaimsContext;
+    
+    /** Context for externally supplied inbound attributes. */
+    @NonnullBeforeExec private AttributeContext attributeContext;
+    
+    /** Store off profile config. */
+    @NonnullBeforeExec private OIDCSSORelyingPartyConfiguration profileConfiguration;
+    
+    /** The set of id_token claims before they are processed.*/
+    @NonnullBeforeExec private JWTClaimsSet unprocessedIdTokenClaims;
+    
+    /** Function used to obtain the issuer ID. */
+    @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
+    
+    /** Function used to obtain the requester ID. */
+    @Nonnull private Function<ProfileRequestContext,String> requesterLookupStrategy;
+    
+    /** Whether to accept decoded attributes that no filter rules applied to. */
+    private boolean acceptUnfilteredAttributes;
+    
+    public ExtractOIDCClaims() {
+        relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+        
+        // prc -> EndUserClaimsContext
+        endUserClaimsContextLookupStrategy = 
+                new ChildContextLookup<>(EndUserClaimsContext.class);
+        
+        // PRC -> EndUserClaimsContext -> AttributeContext
+        attributeContextCreationStrategy = new ChildContextLookup<>(AttributeContext.class, true).compose(
+                new ChildContextLookup<>(EndUserClaimsContext.class));
+        
+        // These appear reversed because we're referring to the attribute issuer and requester,
+        // which is the inverse of the usual assignment of these labels on an inbound assertion.
+        requesterLookupStrategy = new IssuerLookupFunction();
+        issuerLookupStrategy = new RelyingPartyIdLookupFunction();
+        
+    }
+    
+    /**
+     * Set the strategy used to return the {@link RelyingPartyContext} for configuration options.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRelyingPartyContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+        checkSetterPreconditions();
+        relyingPartyContextLookupStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to lookup a {@link EndUserClaimsContext}.
+     * 
+     * @param strategy the strategy
+     */
+    public void setEndUserClaimsContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, EndUserClaimsContext> strategy) {
+        checkSetterPreconditions();
+        
+        endUserClaimsContextLookupStrategy = Constraint.isNotNull(strategy,
+                "EndUserClaimsContextLookupStrategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to create the {@link AttributeContext} to hold results.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setAttributeContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,AttributeContext> strategy) {
+        checkSetterPreconditions();
+        attributeContextCreationStrategy =
+                Constraint.isNotNull(strategy, "AttributeContext creation strategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to locate the attribute requester ID for filtering.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRequesterLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        requesterLookupStrategy = Constraint.isNotNull(strategy, "Requester lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the attribute issuer ID for filtering.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set whether to accept decoded {@link IdPAttribute} objects pulled from the assertions if
+     * no filtering rule applied to them.
+     * 
+     * <p>This is a variant of the original SP's "wildcard" rule support to allow non-enumerated
+     * attributes to be accepted.</p>
+     * 
+     * @param flag
+     */
+    public void setAcceptUnfilteredAttributes(final boolean flag) {
+        checkSetterPreconditions();
+        
+        acceptUnfilteredAttributes = flag;
+    }
+
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        endUserClaimsContext = endUserClaimsContextLookupStrategy.apply(profileRequestContext);
+        if (endUserClaimsContext == null || endUserClaimsContext.getEndUserClaims() == null ||
+                endUserClaimsContext.getUnprocessedIdTokenClaims() == null) {
+            log.debug("{} No EndUserClaimsContext or Claims available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return false;
+        }
+        unprocessedIdTokenClaims = endUserClaimsContext.getUnprocessedIdTokenClaims();
+
+        final RelyingPartyContext rpContext = relyingPartyContextLookupStrategy.apply(profileRequestContext);
+        if (rpContext == null) {
+            log.error("{} Unable to locate RelyingPartyContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+            return false;
+        } else if (rpContext.getProfileConfig() == null) {
+            log.error("{} Unable to locate profile configuration", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        } else if (!(rpContext.getProfileConfig() instanceof OIDCSSORelyingPartyConfiguration)) {
+            log.error("{} Not a OIDC RelyingParty profile configuration", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
+        
+        attributeContext = attributeContextCreationStrategy.apply(profileRequestContext);
+        if (attributeContext == null) {
+            log.debug("{} Unable to create AttributeContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
+        profileConfiguration = (OIDCSSORelyingPartyConfiguration) rpContext.getProfileConfig();
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        processAttributes(profileRequestContext);
+        
+        // Acumulator of all the merged results.
+        final Map<String,IdPAttribute> accumulator = new HashMap<>();
+        accumulator.putAll(attributeContext.getIdPAttributes());
+        
+        //TODO custom extraction, attribute resolution
+        
+        // Install the final result back.
+        attributeContext.setIdPAttributes(accumulator);
+    }
+    
+    /**
+     * Process the inbound OIDC Claims.
+     * 
+     * @param profileRequestContext current profile request context
+     */
+    private void processAttributes(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        log.debug("{} Decoding incoming OIDC Claims", getLogPrefix());
+        
+        final Multimap<String,IdPAttribute> mapped = HashMultimap.create();
+        assert mapped != null;
+
+        final String subject = unprocessedIdTokenClaims.getSubject();        
+        
+        try (final ServiceableComponent<AttributeTranscoderRegistry> component =
+                ensureApplication().getAttributeTranscoderRegistry().getServiceableComponent()) {
+            
+            final ClaimsSet endUserClaims = endUserClaimsContext.getEndUserClaims();
+            assert endUserClaims != null;
+            for (final Map.Entry<String, Object> claim : 
+                endUserClaims.toJSONObject().entrySet()) {                
+                try {    
+                    final JSONObject jsonClaim = new JSONObject();
+                    jsonClaim.put(claim.getKey(), claim.getValue());                
+                    decodeAttribute(component.getComponent(), profileRequestContext, jsonClaim, mapped);
+                } catch (final AttributeDecodingException e) {
+                    log.error("{} Error decoding inbound claim", getLogPrefix(), e);
+                }                    
+            }  
+            
+        } catch (final ServiceException e) {
+            log.error("Attribute transcoder service unavailable", e);
+            return;
+        }
+
+        log.debug("{} Incoming OIDC Claims mapped to attribute IDs: {}", getLogPrefix(), mapped.keySet());
+
+        if (!mapped.isEmpty()) {
+            attributeContext.setUnfilteredIdPAttributes(IdPAttributeSupport.toMapMergeDuplicates(mapped.values()));
+            attributeContext.setIdPAttributes((Map<String,IdPAttribute>) null);
+            filterAttributes(profileRequestContext);
+        }
+    }
+    
+    /**
+     * Check for inbound attributes and apply filtering.
+     * 
+     * @param profileRequestContext current profile request context
+     */
+    private void filterAttributes(@Nonnull final ProfileRequestContext profileRequestContext) {
+                
+        final AttributeFilterContext filterContext =
+                endUserClaimsContext.ensureSubcontext(AttributeFilterContext.class);   
+        
+        populateFilterContext(profileRequestContext, filterContext);
+        
+        try (final ServiceableComponent<AttributeFilter> filterComponent =
+                ensureApplication().getAttributeFilter().getServiceableComponent();
+                final ServiceableComponent<MetadataResolver> metadataResolverComponent =
+                        ensureApplication().getMetadataResolver().getServiceableComponent()) {
+            
+            // Populate here for locking scope.
+            filterContext.setMetadataResolver(metadataResolverComponent.getComponent());
+            
+            final AttributeFilter filter = filterComponent.getComponent();
+            filter.filterAttributes(filterContext);
+            filterContext.removeFromParent();
+            attributeContext.setIdPAttributes(filterContext.getFilteredIdPAttributes());
+        } catch (final AttributeFilterException e) {
+            log.error("{} Error while filtering inbound attributes", getLogPrefix(), e);
+        } catch (final ServiceException e) {
+            log.error("{} Invalid AttributeFilter configuration", getLogPrefix(), e);
+        } 
+    }
+    
+    /**
+     * Fill in the filter context data.
+     * 
+     * @param profileRequestContext current profile request context
+     * @param filterContext context to populate
+     */
+    private void populateFilterContext(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AttributeFilterContext filterContext) {
+        
+        final AttributeContext ac = attributeContext;
+        assert ac != null;
+        
+        filterContext.setDirection(Direction.INBOUND)
+            .setPrefilteredIdPAttributes(attributeContext.getUnfilteredIdPAttributes())
+            .setRequesterMetadataContextLookupStrategy(null)
+            .setIssuerMetadataContextLookupStrategy(
+                    new SAMLMetadataContextLookupFunction().compose(
+                            new RecursiveTypedParentContextLookup<>(ProfileRequestContext.class)))
+            .setProxiedRequesterContextLookupStrategy(null)
+            .setAttributeIssuerID(issuerLookupStrategy.apply(profileRequestContext))
+            .setAttributeRecipientID(requesterLookupStrategy.apply(profileRequestContext))
+            //TODO add this back
+            //.setAttributeRecipientGroupID(profileConfiguration.getAttributeRecipientGroupID(profileRequestContext))
+            .setIncludeUnfilteredAttributes(acceptUnfilteredAttributes);
+    }
+    
+    /**
+     * Access the registry of transcoding rules to transform (decode) the input claims to IdP Attributes.
+     * 
+     * @param registry  registry of transcoding rules
+     * @param profileRequestContext current profile request context
+     * @param input input attribute
+     * @param results collection to add results to
+     * 
+     * @throws AttributeDecodingException if a non-ignorable error occurs
+     */
+    private void decodeAttribute(@Nonnull final AttributeTranscoderRegistry registry,
+            @Nonnull final ProfileRequestContext profileRequestContext, @Nonnull final JSONObject input,
+            @Nonnull @NonnullElements @Live final Multimap<String,IdPAttribute> results)
+                    throws AttributeDecodingException {
+        
+        final Collection<TranscodingRule> transcodingRules = registry.getTranscodingRules(input);
+        if (transcodingRules.isEmpty()) {
+            log.debug("{} No transcoding rule for Attribute '{}'", getLogPrefix(), input);
+            return;
+        }
+        
+        for (final TranscodingRule rules : transcodingRules) {
+            assert rules != null;
+            final AttributeTranscoder<JSONObject> transcoder = TranscoderSupport.getTranscoder(rules);
+            final IdPAttribute decodedAttribute = transcoder.decode(profileRequestContext, input, rules);
+            if (decodedAttribute != null) {
+                results.put(decodedAttribute.getId(), decodedAttribute);
+            }
+        }
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareAgentResponse.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareAgentResponse.java
new file mode 100644
index 0000000..7379a31
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareAgentResponse.java
@@ -0,0 +1,35 @@
+/*
+ * 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 net.shibboleth.sp.oidc.profile.impl;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.sp.profile.AbstractTokenConsumerResponseAction;
+
+/**
+ * OIDC-specific subclass of a token consumer response action.
+ * 
+ * TODO...
+ */
+public class PrepareAgentResponse extends AbstractTokenConsumerResponseAction {
+
+    /** {@inheritDoc} */
+    @Override
+    protected String getSessionData(final ProfileRequestContext profileRequestContext) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaims.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaims.java
index 859c88d..6fa289e 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaims.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaims.java
@@ -102,10 +102,9 @@ public class ProcessEndUserClaims extends AbstractProfileAction {
                 new ChildContextLookup<>(AccessTokenResponseContext.class).compose(
                         new InboundMessageContextLookup());
         
-        // Will create context.
+        // Will create context. prc -> EndUserClaimsContext
         endUserClaimsContextLookupStrategy = 
-                new ChildContextLookup<>(EndUserClaimsContext.class, true).compose(
-                        new InboundMessageContextLookup());
+                new ChildContextLookup<>(EndUserClaimsContext.class, true);
         
         claimMergingStrategy = new DefaultClaimMergingStrategy();
         claimSanitizationStrategy = new DefaultClaimSanitizationStrategy();

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list