[java-plugin-shibd] 02/02: Finish initial work on SAML 2 request action and add unit tests.
Scott Cantor
cantor.2 at osu.edu
Wed Aug 7 16:38:09 UTC 2024
This is an automated email from the git hooks/post-receive script.
scantor pushed a commit to branch main
in repository java-plugin-shibd.
View the commit online:
http://git.shibboleth.net/view/?p=java-plugin-shibd.git;a=commit;h=7a8df0ee973f4bbe9c17f68b84f9d0f126fbf833
commit 7a8df0ee973f4bbe9c17f68b84f9d0f126fbf833
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Aug 7 12:38:04 2024 -0400
Finish initial work on SAML 2 request action and add unit tests.
---
.../flows/saml2/SAML2SessionInitiatorFlowTest.java | 223 ++++++++++++++++++++-
.../shibboleth/idp/module/conf/sp/test-agents.xml | 15 ++
.../saml/saml2/profile/impl/AddAuthnRequest.java | 111 +++++-----
3 files changed, 295 insertions(+), 54 deletions(-)
diff --git a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/saml2/SAML2SessionInitiatorFlowTest.java b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/saml2/SAML2SessionInitiatorFlowTest.java
index e230246..91a1383 100644
--- a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/saml2/SAML2SessionInitiatorFlowTest.java
+++ b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/saml2/SAML2SessionInitiatorFlowTest.java
@@ -18,12 +18,19 @@ import java.io.IOException;
import java.time.Instant;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.opensaml.messaging.decoder.MessageDecodingException;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.saml.common.SAMLObject;
+import org.opensaml.saml.saml2.core.AuthnContext;
+import org.opensaml.saml.saml2.core.AuthnContextClassRef;
+import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.NameIDPolicy;
+import org.opensaml.saml.saml2.core.NameIDType;
+import org.opensaml.saml.saml2.core.RequestedAuthnContext;
+import org.opensaml.saml.saml2.metadata.NameIDFormat;
import org.springframework.webflow.executor.FlowExecutionResult;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -142,8 +149,7 @@ public class SAML2SessionInitiatorFlowTest extends AbstractSPFlowTest {
assertFlowExecutionOutcome(result.getOutcome());
assertOutputMessageEvent(result, null);
- final AuthnRequest req = validateOutputMessage(result);
- Assert.assertFalse(req.isSigned());
+ final AuthnRequest req = validateOutputMessage(result, null);
Assert.assertNull(req.getSubject());
Assert.assertNull(req.getRequestedAuthnContext());
Assert.assertNull(req.getScoping());
@@ -151,15 +157,225 @@ public class SAML2SessionInitiatorFlowTest extends AbstractSPFlowTest {
assertFalse(req.isPassive());
}
+ /**
+ * Test success supplying ForceAuthn from agent.
+ *
+ * @throws IOException
+ * @throws MessageDecodingException
+ */
+ @Test
+ public void testForceAuthnFromAgent() throws IOException, MessageDecodingException {
+ setDefaultAuth();
+
+ final DDF input = new DDF(null).structure();
+ input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+ input.addmember(AuthnRequest.FORCE_AUTHN_ATTRIB_NAME).integer(1);
+ setApplicationRequest(APPLICATION_ID, input);
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, FLOW_ID);
+ assertFlowExecutionOutcome(result.getOutcome());
+
+ assertOutputMessageEvent(result, null);
+ final AuthnRequest req = validateOutputMessage(result, null);
+ Assert.assertNull(req.getSubject());
+ Assert.assertNull(req.getRequestedAuthnContext());
+ Assert.assertNull(req.getScoping());
+ assertTrue(req.isForceAuthn());
+ assertFalse(req.isPassive());
+ }
+
+ /**
+ * Test failure supplying ForceAuthn from agent.
+ *
+ * @throws IOException
+ * @throws MessageDecodingException
+ */
+ @Test
+ public void testForceAuthnDisallowed() throws IOException, MessageDecodingException {
+ setDefaultAuth();
+
+ final DDF input = new DDF(null).structure();
+ input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+ input.addmember(AuthnRequest.FORCE_AUTHN_ATTRIB_NAME).integer(1);
+ setApplicationRequest("feature-blocking", input);
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, FLOW_ID);
+ assertFlowExecutionOutcome(result.getOutcome());
+
+ assertOutputMessageEvent(result, null);
+ final AuthnRequest req = validateOutputMessage(result, null);
+ Assert.assertNull(req.getSubject());
+ Assert.assertNull(req.getRequestedAuthnContext());
+ Assert.assertNull(req.getScoping());
+ assertFalse(req.isForceAuthn());
+ assertFalse(req.isPassive());
+ }
+
+ /**
+ * Test success supplying IsPassive from agent.
+ *
+ * @throws IOException
+ * @throws MessageDecodingException
+ */
+ @Test
+ public void testIsPassiveFromAgent() throws IOException, MessageDecodingException {
+ setDefaultAuth();
+
+ final DDF input = new DDF(null).structure();
+ input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+ input.addmember(AuthnRequest.IS_PASSIVE_ATTRIB_NAME).integer(1);
+ setApplicationRequest(APPLICATION_ID, input);
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, FLOW_ID);
+ assertFlowExecutionOutcome(result.getOutcome());
+
+ assertOutputMessageEvent(result, null);
+ final AuthnRequest req = validateOutputMessage(result, null);
+ Assert.assertNull(req.getSubject());
+ Assert.assertNull(req.getRequestedAuthnContext());
+ Assert.assertNull(req.getScoping());
+ assertFalse(req.isForceAuthn());
+ assertTrue(req.isPassive());
+ }
+
+ /**
+ * Test success supplying NameIDPolicy format from agent.
+ *
+ * @throws IOException
+ * @throws MessageDecodingException
+ */
+ @Test
+ public void testNameIDPolicyFromAgent() throws IOException, MessageDecodingException {
+ setDefaultAuth();
+
+ final DDF input = new DDF(null).structure();
+ input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+ input.addmember(NameIDFormat.DEFAULT_ELEMENT_LOCAL_NAME).string(NameIDType.EMAIL);
+ setApplicationRequest(APPLICATION_ID, input);
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, FLOW_ID);
+ assertFlowExecutionOutcome(result.getOutcome());
+
+ assertOutputMessageEvent(result, null);
+ final AuthnRequest req = validateOutputMessage(result, NameIDType.EMAIL);
+ Assert.assertNull(req.getSubject());
+ Assert.assertNull(req.getRequestedAuthnContext());
+ Assert.assertNull(req.getScoping());
+ assertFalse(req.isForceAuthn());
+ assertFalse(req.isPassive());
+ }
+
+ /**
+ * Test failure supplying NameIDPolicy format from agent.
+ *
+ * @throws IOException
+ * @throws MessageDecodingException
+ */
+ @Test
+ public void testNameIDPolicyDisallowed() throws IOException, MessageDecodingException {
+ setDefaultAuth();
+
+ final DDF input = new DDF(null).structure();
+ input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+ input.addmember(NameIDFormat.DEFAULT_ELEMENT_LOCAL_NAME).string(NameIDType.EMAIL);
+ setApplicationRequest("feature-blocking", input);
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, FLOW_ID);
+ assertFlowExecutionOutcome(result.getOutcome());
+
+ assertOutputMessageEvent(result, null);
+ final AuthnRequest req = validateOutputMessage(result, null);
+ Assert.assertNull(req.getSubject());
+ Assert.assertNull(req.getRequestedAuthnContext());
+ Assert.assertNull(req.getScoping());
+ assertFalse(req.isForceAuthn());
+ assertFalse(req.isPassive());
+ }
+
+ /**
+ * Test success supplying RequestedAuthnContext from agent.
+ *
+ * @throws IOException
+ * @throws MessageDecodingException
+ */
+ @Test
+ public void testAuthnContextFromAgent() throws IOException, MessageDecodingException {
+ setDefaultAuth();
+
+ final DDF input = new DDF(null).structure();
+ input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+ final DDF aclist = input.addmember(AuthnContextClassRef.DEFAULT_ELEMENT_LOCAL_NAME).list();
+ aclist.add(new DDF(null).string(AuthnContext.X509_AUTHN_CTX));
+ aclist.add(new DDF(null).string(AuthnContext.TIME_SYNC_TOKEN_AUTHN_CTX));
+ input.addmember("AuthnContextComparison").string(AuthnContextComparisonTypeEnumeration.MINIMUM.toString());
+ setApplicationRequest(APPLICATION_ID, input);
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, FLOW_ID);
+ assertFlowExecutionOutcome(result.getOutcome());
+
+ assertOutputMessageEvent(result, null);
+ final AuthnRequest req = validateOutputMessage(result, null);
+ Assert.assertNull(req.getSubject());
+ Assert.assertNull(req.getScoping());
+ assertFalse(req.isForceAuthn());
+ assertFalse(req.isPassive());
+
+ final RequestedAuthnContext rac = req.getRequestedAuthnContext();
+ assert rac != null;
+ Assert.assertEquals(rac.getComparison(), AuthnContextComparisonTypeEnumeration.MINIMUM);
+ Assert.assertEquals(rac.getAuthnContextClassRefs().size(), 2);
+ Assert.assertEquals(rac.getAuthnContextClassRefs().get(0).getURI(), AuthnContext.X509_AUTHN_CTX);
+ Assert.assertEquals(rac.getAuthnContextClassRefs().get(1).getURI(), AuthnContext.TIME_SYNC_TOKEN_AUTHN_CTX);
+ }
+
+ /**
+ * Test success supplying RequestedAuthnContext from agent.
+ *
+ * @throws IOException
+ * @throws MessageDecodingException
+ */
+ @Test
+ public void testAuthnContextDisallowed() throws IOException, MessageDecodingException {
+ setDefaultAuth();
+
+ final DDF input = new DDF(null).structure();
+ input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+ final DDF aclist = input.addmember(AuthnContextClassRef.DEFAULT_ELEMENT_LOCAL_NAME).list();
+ aclist.add(new DDF(null).string(AuthnContext.X509_AUTHN_CTX));
+ aclist.add(new DDF(null).string(AuthnContext.TIME_SYNC_TOKEN_AUTHN_CTX));
+ setApplicationRequest("feature-blocking", input);
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, FLOW_ID);
+ assertFlowExecutionOutcome(result.getOutcome());
+
+ assertOutputMessageEvent(result, null);
+ final AuthnRequest req = validateOutputMessage(result, null);
+ Assert.assertNull(req.getSubject());
+ Assert.assertNull(req.getRequestedAuthnContext());
+ Assert.assertNull(req.getScoping());
+ assertFalse(req.isForceAuthn());
+ assertFalse(req.isPassive());
+ }
+
/**
* Decode an encoded response and run sanity checks against it.
*
* @param result flow execution result
+ * @param format NameID format to check for in policy element
+ *
* @return the request object
*
* @throws MessageDecodingException
*/
- @Nonnull private AuthnRequest validateOutputMessage(@Nonnull final FlowExecutionResult result) throws MessageDecodingException {
+ @Nonnull private AuthnRequest validateOutputMessage(@Nonnull final FlowExecutionResult result, @Nullable final String format)
+ throws MessageDecodingException {
final ProfileRequestContext prc = retrieveProfileRequestContext(result);
assert prc != null;
final AgentRequestContext arc = prc.ensureSubcontext(AgentRequestContext.class);
@@ -195,6 +411,7 @@ public class SAML2SessionInitiatorFlowTest extends AbstractSPFlowTest {
final NameIDPolicy pol = authnRequest.getNameIDPolicy();
assert pol != null;
assertTrue(pol.getAllowCreate());
+ Assert.assertEquals(pol.getFormat(), format);
return authnRequest;
}
diff --git a/sp-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/test-agents.xml b/sp-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/test-agents.xml
index c12021f..e9e80df 100644
--- a/sp-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/test-agents.xml
+++ b/sp-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/test-agents.xml
@@ -19,6 +19,11 @@
p:issuer="https://testsp.example.org"
p:authenticatingAuthority="https://idp.example.org" />
+ <bean p:id="feature-blocking" parent="shibboleth.Application"
+ p:issuer="https://testsp.example.org"
+ p:authenticatingAuthority="https://idp.example.org"
+ p:defaultConfiguration-ref="featureBlockingDefaultRelyingParty" />
+
<bean p:id="no-metadata" parent="shibboleth.Application"
p:issuer="https://testsp.example.org"
p:authenticatingAuthority="https://unknown.example.org" />
@@ -40,4 +45,14 @@
</property>
</bean>
+ <bean id="featureBlockingDefaultRelyingParty" parent="RelyingParty">
+ <property name="profileConfigurations">
+ <list>
+ <bean parent="SAML2.SSO" p:disallowedFeatures="0x1F" />
+ <ref bean="SAML2.ECP" />
+ <ref bean="SAML2.Logout" />
+ </list>
+ </property>
+ </bean>
+
</beans>
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
index c7b7c96..ef5ad74 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
@@ -41,6 +41,7 @@ import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.core.xml.io.UnmarshallingException;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
@@ -57,7 +58,6 @@ import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.NameIDPolicy;
import org.opensaml.saml.saml2.core.RequestedAuthnContext;
-import org.opensaml.saml.saml2.core.RequesterID;
import org.opensaml.saml.saml2.core.Scoping;
import org.opensaml.saml.saml2.core.Subject;
import org.opensaml.saml.saml2.metadata.NameIDFormat;
@@ -91,13 +91,19 @@ public class AddAuthnRequest extends AbstractApplicationAction {
/** Overwrite an existing message? */
private boolean overwriteExisting;
+
+ /** Controls whether to include {@link Scoping} element. */
+ private boolean includeScoping;
+
+ /** Strategy used to locate {@link RelyingPartyContext} if required. */
+ @Nullable private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
/** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
@Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
/** Strategy used to obtain the request issuer value. */
@Nullable private Function<ProfileRequestContext,String> issuerLookupStrategy;
-
+
/** Optional strategy to populate request with a {@link NameID}. */
@Nullable private Function<ProfileRequestContext,NameID> nameIDLookupStrategy;
@@ -117,6 +123,8 @@ public class AddAuthnRequest extends AbstractApplicationAction {
public AddAuthnRequest() {
// Default strategy is a 16-byte secure random source.
idGeneratorLookupStrategy = new IdentifierGenerationStrategyLookupFunction();
+
+ relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
}
/**
@@ -128,6 +136,31 @@ public class AddAuthnRequest extends AbstractApplicationAction {
checkSetterPreconditions();
overwriteExisting = flag;
}
+
+ /**
+ * Set whether to include {@link Scoping} in request carrying a known entityID
+ * in an {@link IDPList}.
+ *
+ * <p>Defaults to false.</p>
+ *
+ * @param flag flag to set
+ */
+ public void setIncludeScoping(final boolean flag) {
+ checkSetterPreconditions();
+ includeScoping = true;
+ }
+
+ /**
+ * Set lookup strategy to locate {@link RelyingPartyContext}, only relevant when
+ * also including {@link Scoping} element.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setRelyingPartyContextLookupStrategy(
+ @Nullable final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+ checkSetterPreconditions();
+ relyingPartyContextLookupStrategy = strategy;
+ }
/**
* Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
@@ -268,12 +301,13 @@ public class AddAuthnRequest extends AbstractApplicationAction {
object.setNameIDPolicy(buildNameIDPolicy(profileRequestContext, nipBuilder));
object.setRequestedAuthnContext(buildRequestedAuthnContext(profileRequestContext));
object.setSubject(buildSubject(profileRequestContext));
- object.setScoping(buildScoping(profileRequestContext));
object.setExtensions(buildExtensions(profileRequestContext));
+
+ if (includeScoping) {
+ object.setScoping(buildScoping(profileRequestContext));
+ }
- final MessageContext omc = profileRequestContext.getOutboundMessageContext();
- assert omc != null;
- omc.setMessage(object);
+ profileRequestContext.ensureOutboundMessageContext().setMessage(object);
log.info("{} Generated AuthnRequest with ID {} from {}", getLogPrefix(), object.getID(), issuerId);
}
@@ -366,7 +400,7 @@ public class AddAuthnRequest extends AbstractApplicationAction {
@Nullable private RequestedAuthnContext buildRequestedAuthnContext(
@Nullable final ProfileRequestContext profileRequestContext) {
- List<String> classrefs = input.getmember(AuthnContextClassRef.DEFAULT_ELEMENT_LOCAL_NAME).list().asList()
+ List<String> classrefs = input.getmember(AuthnContextClassRef.DEFAULT_ELEMENT_LOCAL_NAME).asList()
.stream()
.map(DDF::string)
.collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
@@ -414,7 +448,7 @@ public class AddAuthnRequest extends AbstractApplicationAction {
final AuthnContextComparisonTypeEnumeration operator;
if (opstring != null) {
try {
- operator = AuthnContextComparisonTypeEnumeration.valueOf(opstring);
+ operator = AuthnContextComparisonTypeEnumeration.valueOf(opstring.toUpperCase());
} catch (final IllegalArgumentException e) {
log.warn("{} Error translating RequestedAuthnContext operator string", getLogPrefix(), e);
return null;
@@ -468,57 +502,32 @@ public class AddAuthnRequest extends AbstractApplicationAction {
*/
@Nullable private Scoping buildScoping(@Nonnull final ProfileRequestContext profileRequestContext) {
- boolean include = false;
-
- // TODO: may need to add this back to PC
- // assert profileConfiguration != null;
- //if (profileConfiguration.isIgnoreScoping(profileRequestContext)) {
- // log.warn("{} Skipping generation of Scoping element in violation of standard", getLogPrefix());
- // return null;
- //}
+ final RelyingPartyContext rpContext = relyingPartyContextLookupStrategy != null ?
+ relyingPartyContextLookupStrategy.apply(profileRequestContext) : null;
+ final String entityID = rpContext != null ? rpContext.getRelyingPartyId() : null;
+ if (entityID == null) {
+ return null;
+ }
final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
final SAMLObjectBuilder<Scoping> scopingBuilder =
(SAMLObjectBuilder<Scoping>) bf.<Scoping>ensureBuilder(Scoping.DEFAULT_ELEMENT_NAME);
- final Scoping scoping = scopingBuilder.buildObject();
-
- // TODO: from agent
- if (false) {
- //scoping.setProxyCount(Integer.max(0, count - 1));
- include = true;
- }
-
- // TODO: from agent
- if (false) {
- final SAMLObjectBuilder<IDPList> idpListBuilder =
- (SAMLObjectBuilder<IDPList>) bf.<IDPList>ensureBuilder(IDPList.DEFAULT_ELEMENT_NAME);
- final SAMLObjectBuilder<IDPEntry> idpBuilder =
- (SAMLObjectBuilder<IDPEntry>) bf.<IDPEntry>ensureBuilder(IDPEntry.DEFAULT_ELEMENT_NAME);
-
- final IDPList idps = idpListBuilder.buildObject();
- for (final String idp : CollectionSupport.<String>emptyList()) {
- final IDPEntry entry = idpBuilder.buildObject();
- entry.setProviderID(idp);
- idps.getIDPEntrys().add(entry);
- }
- scoping.setIDPList(idps);
- include = true;
- }
+ final SAMLObjectBuilder<IDPList> idpListBuilder =
+ (SAMLObjectBuilder<IDPList>) bf.<IDPList>ensureBuilder(IDPList.DEFAULT_ELEMENT_NAME);
+ final SAMLObjectBuilder<IDPEntry> idpBuilder =
+ (SAMLObjectBuilder<IDPEntry>) bf.<IDPEntry>ensureBuilder(IDPEntry.DEFAULT_ELEMENT_NAME);
- final SAMLObjectBuilder<RequesterID> requesterIdBuilder =
- (SAMLObjectBuilder<RequesterID>) bf.<RequesterID>ensureBuilder(RequesterID.DEFAULT_ELEMENT_NAME);
+ final IDPEntry entry = idpBuilder.buildObject();
+ entry.setProviderID(entityID);
- // TODO: from agent
- final String immediateRequester = null;
- if (immediateRequester != null) {
- final RequesterID requesterId = requesterIdBuilder.buildObject();
- requesterId.setURI(immediateRequester);
- scoping.getRequesterIDs().add(requesterId);
- include = true;
- }
+ final IDPList idps = idpListBuilder.buildObject();
+ idps.getIDPEntrys().add(entry);
- return include ? scoping : null;
+ final Scoping scoping = scopingBuilder.buildObject();
+ scoping.setIDPList(idps);
+
+ return scoping;
}
/**
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list