[java-identity-provider] branch main updated: IDP-2467 LdapPrincipal account state support
Codeberg
noreply at shibboleth.net
Wed Jul 1 03:01:59 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-identity-provider.
View the commit online:
https://codeberg.org/Shibboleth/java-identity-provider/commit/c60924e9a1a00945b8a87ea63b423c591ae01400
The following commit(s) were added to refs/heads/main by this push:
new c60924e9a IDP-2467 LdapPrincipal account state support
c60924e9a is described below
commit c60924e9a1a00945b8a87ea63b423c591ae01400
Author: Daniel Fisher <dfisher at vt.edu>
AuthorDate: Tue Jun 30 22:22:34 2026 -0400
IDP-2467 LdapPrincipal account state support
https://shibboleth.atlassian.net/browse/IDP-2467
Update LDAPCredentialValidator to set account state.
Update LDAPPrincipalSerializer to support account state.
---
.../idp/authn/impl/LDAPCredentialValidator.java | 5 +-
.../principal/impl/LDAPPrincipalSerializer.java | 98 +++++++++++-
.../DefaultAuthenticationResultSerializerTest.java | 55 +++++++
.../impl/LDAPPrincipalSerializerTest.java | 172 +++++++++++++++++++++
.../impl/LDAPAuthenticationAccountStateResult.json | 1 +
5 files changed, 324 insertions(+), 7 deletions(-)
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/LDAPCredentialValidator.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/LDAPCredentialValidator.java
index 426202fed..28c3b021b 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/LDAPCredentialValidator.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/LDAPCredentialValidator.java
@@ -223,7 +223,10 @@ public class LDAPCredentialValidator extends AbstractUsernamePasswordCredentialV
final Subject subject = new Subject();
subject.getPrincipals().add(
- new LdapPrincipal(usernamePasswordContext.getTransformedUsername(), ldapResponse.getLdapEntry()));
+ new LdapPrincipal(
+ usernamePasswordContext.getTransformedUsername(),
+ ldapResponse.getLdapEntry(),
+ ldapResponse.getAccountState()));
return super.populateSubject(subject, usernamePasswordContext);
}
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/principal/impl/LDAPPrincipalSerializer.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/principal/impl/LDAPPrincipalSerializer.java
index d72e9171d..cdb6b315c 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/principal/impl/LDAPPrincipalSerializer.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/principal/impl/LDAPPrincipalSerializer.java
@@ -18,14 +18,18 @@ import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.security.Principal;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
+import javax.security.auth.login.LoginException;
import jakarta.json.Json;
-import jakarta.json.JsonArray;
import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonException;
@@ -39,6 +43,7 @@ import jakarta.json.stream.JsonGenerator;
import org.ldaptive.LdapAttribute;
import org.ldaptive.LdapEntry;
+import org.ldaptive.auth.AccountState;
import org.ldaptive.jaas.LdapPrincipal;
import org.slf4j.Logger;
@@ -60,13 +65,17 @@ public class LDAPPrincipalSerializer extends AbstractPrincipalSerializer<String>
/** Field name of principal entry. */
@Nonnull @NotEmpty private static final String PRINCIPAL_ENTRY_FIELD = "LDAPE";
+ /** Field name of principal account state. */
+ @Nonnull @NotEmpty private static final String PRINCIPAL_ACCOUNT_STATE_FIELD = "LDAPAS";
+
/** Pattern used to determine if input is supported. */
- private static final Pattern JSON_PATTERN = Pattern.compile("^\\{\"LDAPN\":.*,\"LDAPE\":.*\\}$");
+ private static final Pattern JSON_PATTERN = Pattern.compile(
+ "^\\{(\"LDAPN\":.+)(,\"LDAPE\":.+)?(,\"LDAPAS\":.+)?\\}$");
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(LDAPPrincipalSerializer.class);
- /** JSON object bulder factory. */
+ /** JSON object builder factory. */
@Nonnull private final JsonBuilderFactory objectBuilderFactory;
/** Constructor. */
@@ -100,6 +109,34 @@ public class LDAPPrincipalSerializer extends AbstractPrincipalSerializer<String>
}
gen.write(PRINCIPAL_ENTRY_FIELD, objectBuilder.build());
}
+ final AccountState accountState = ((LdapPrincipal) principal).getAccountState();
+ if (accountState != null) {
+ final JsonObjectBuilder objectBuilder = getJsonObjectBuilder();
+ if (accountState.getWarnings() != null) {
+ final JsonArrayBuilder warningsBuilder = getJsonArrayBuilder();
+ for (final AccountState.Warning warning : accountState.getWarnings()) {
+ final JsonObjectBuilder warningBuilder = getJsonObjectBuilder();
+ if (warning.getExpiration() != null) {
+ warningBuilder.add(
+ "exp", warning.getExpiration().format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
+ }
+ warningBuilder.add("loginsRemaining", warning.getLoginsRemaining());
+ warningsBuilder.add(warningBuilder);
+ }
+ objectBuilder.add("warnings", warningsBuilder.build());
+ }
+ if (accountState.getErrors() != null) {
+ final JsonArrayBuilder errorsBuilder = getJsonArrayBuilder();
+ for (final AccountState.Error error : accountState.getErrors()) {
+ final JsonObjectBuilder errorBuilder = getJsonObjectBuilder();
+ errorBuilder.add("code", error.getCode());
+ errorBuilder.add("msg", error.getMessage());
+ errorsBuilder.add(errorBuilder);
+ }
+ objectBuilder.add("errors", errorsBuilder.build());
+ }
+ gen.write(PRINCIPAL_ACCOUNT_STATE_FIELD, objectBuilder.build());
+ }
gen.writeEnd();
}
final String result = sink.toString();
@@ -122,7 +159,7 @@ public class LDAPPrincipalSerializer extends AbstractPrincipalSerializer<String>
throw new IOException("Found invalid data structure while parsing LdapPrincipal");
}
- final JsonObject obj = (JsonObject) st;
+ final JsonObject obj = st.asJsonObject();
final JsonString str = obj.getJsonString(PRINCIPAL_NAME_FIELD);
if (str != null) {
if (!Strings.isNullOrEmpty(str.getString())) {
@@ -135,14 +172,45 @@ public class LDAPPrincipalSerializer extends AbstractPrincipalSerializer<String>
entry.setDn(((JsonString) e.getValue()).getString());
} else {
final LdapAttribute attr = new LdapAttribute(e.getKey());
- for (final JsonValue v : (JsonArray) e.getValue()) {
+ for (final JsonValue v : e.getValue().asJsonArray()) {
attr.addStringValues(((JsonString) v).getString());
}
entry.addAttributes(attr);
}
}
}
- return new LdapPrincipal(str.getString(), entry);
+ AccountState accountState = null;
+ final JsonObject jsonAccountState = obj.getJsonObject(PRINCIPAL_ACCOUNT_STATE_FIELD);
+ if (jsonAccountState != null) {
+ final List<AccountState.Error> errors = new ArrayList<>();
+ final List<AccountState.Warning> warnings = new ArrayList<>();
+ for (final Map.Entry<String, JsonValue> e : jsonAccountState.entrySet()) {
+ if ("errors".equalsIgnoreCase(e.getKey())) {
+ for (final JsonValue v : e.getValue().asJsonArray()) {
+ final JsonObject o = v.asJsonObject();
+ errors.add(new DeserializedError(o.getInt("code"), o.getString("msg")));
+ }
+ } else if ("warnings".equalsIgnoreCase(e.getKey())) {
+ for (final JsonValue v : e.getValue().asJsonArray()) {
+ final JsonObject o = v.asJsonObject();
+ ZonedDateTime zdt = null;
+ if (o.containsKey("exp")) {
+ zdt = ZonedDateTime.parse(
+ o.getString("exp"), DateTimeFormatter.ISO_ZONED_DATE_TIME);
+ }
+ int lr = -1;
+ if (o.containsKey("loginsRemaining")) {
+ lr = o.getInt("loginsRemaining");
+ }
+ warnings.add(new AccountState.DefaultWarning(zdt, lr));
+ }
+ }
+ }
+ accountState = new AccountState(
+ warnings.isEmpty() ? null : warnings.toArray(new AccountState.Warning[0]),
+ errors.isEmpty() ? null : errors.toArray(new AccountState.Error[0]));
+ }
+ return new LdapPrincipal(str.getString(), entry, accountState);
}
log.warn("Skipping null/empty LdapPrincipal");
}
@@ -174,4 +242,22 @@ public class LDAPPrincipalSerializer extends AbstractPrincipalSerializer<String>
return result;
}
+ /** Error implementation that stores deserialized properties. */
+ private record DeserializedError(int code, String message) implements AccountState.Error {
+
+ @Override
+ public int getCode() {
+ return code;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ @Override
+ public void throwSecurityException() throws LoginException {
+ throw new UnsupportedOperationException("This method is not supported");
+ }
+ }
}
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/DefaultAuthenticationResultSerializerTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/DefaultAuthenticationResultSerializerTest.java
index e53036c6e..5dd83019f 100644
--- a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/DefaultAuthenticationResultSerializerTest.java
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/DefaultAuthenticationResultSerializerTest.java
@@ -31,6 +31,7 @@ import javax.security.auth.Subject;
import org.ldaptive.LdapAttribute;
import org.ldaptive.LdapEntry;
+import org.ldaptive.auth.ext.PasswordPolicyAccountState;
import org.ldaptive.jaas.LdapPrincipal;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.profile.testing.RequestContextBuilder;
@@ -413,6 +414,60 @@ public class DefaultAuthenticationResultSerializerTest {
assertTrue(result2.getReuseCondition().test(prc));
}
+ @Test public void testLdapWithAccountState() throws Exception {
+ serializer.initialize();
+ flowDescriptor.initialize();
+
+ final AuthenticationResult result = createResult(flowDescriptor, new Subject());
+ final LdapEntry entry = new LdapEntry();
+ entry.setDn("uid=1234,ou=people,dc=shibboleth,dc=net");
+ final LdapAttribute givenName = new LdapAttribute();
+ givenName.setName("givenName");
+ givenName.addStringValues("Bob", "Robert");
+ entry.addAttributes(
+ new LdapAttribute("cn", "Bob Cobb"),
+ givenName,
+ new LdapAttribute("sn", "Cobb"),
+ new LdapAttribute("mail", "bob at shibboleth.net"));
+ final PasswordPolicyAccountState accountState = new PasswordPolicyAccountState(5);
+ result.getSubject().getPrincipals().add(new LdapPrincipal("bob", LdapEntry.sort(entry), accountState));
+
+ final ProfileRequestContext prc = getProfileRequestContext(CollectionSupport.singletonList(flowDescriptor));
+ assertTrue(result.getReuseCondition().test(prc));
+
+ final String s = flowDescriptor.serialize(result);
+ final String s2 = fileToString(DATAPATH + "LDAPAuthenticationAccountStateResult.json");
+ assertEquals(s, s2);
+
+ final AuthenticationResult result2 = flowDescriptor.deserialize(1, CONTEXT, KEY, s2,
+ Instant.ofEpochMilli(ACTIVITY)
+ .plus(flowDescriptor.getInactivityTimeout())
+ .plus(AuthenticationFlowDescriptor.STORAGE_EXPIRATION_OFFSET)
+ .toEpochMilli());
+
+ assertEquals(result.getAuthenticationFlowId(), result2.getAuthenticationFlowId());
+ assertEquals(result.getAuthenticationInstant(), result2.getAuthenticationInstant());
+ assertEquals(result.getLastActivityInstant(), result2.getLastActivityInstant());
+ assertEquals(result.getSubject(), result2.getSubject());
+ assertEquals(
+ ((LdapPrincipal) result.getSubject().getPrincipals().iterator().next()).getLdapEntry(),
+ ((LdapPrincipal) result2.getSubject().getPrincipals().iterator().next()).getLdapEntry());
+ assertEquals(
+ ((LdapPrincipal) result.getSubject().getPrincipals().iterator().next()).getAccountState().getError(),
+ ((LdapPrincipal) result2.getSubject().getPrincipals().iterator().next()).getAccountState().getError());
+ assertEquals(
+ ((LdapPrincipal) result.getSubject().getPrincipals().iterator().next())
+ .getAccountState().getWarning().getExpiration(),
+ ((LdapPrincipal) result2.getSubject().getPrincipals().iterator().next())
+ .getAccountState().getWarning().getExpiration());
+ assertEquals(
+ ((LdapPrincipal) result.getSubject().getPrincipals().iterator().next())
+ .getAccountState().getWarning().getLoginsRemaining(),
+ ((LdapPrincipal) result2.getSubject().getPrincipals().iterator().next())
+ .getAccountState().getWarning().getLoginsRemaining());
+ assertTrue(result2.getReuseCondition().test(prc));
+ }
+
@Test public void testIdPAttribute() throws Exception {
serializer.initialize();
flowDescriptor.initialize();
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/principal/impl/LDAPPrincipalSerializerTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/principal/impl/LDAPPrincipalSerializerTest.java
new file mode 100644
index 000000000..904c2108c
--- /dev/null
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/principal/impl/LDAPPrincipalSerializerTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.idp.authn.principal.impl;
+
+import java.io.IOException;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import javax.security.auth.x500.X500Principal;
+
+import org.ldaptive.LdapAttribute;
+import org.ldaptive.LdapEntry;
+import org.ldaptive.auth.ext.PasswordPolicyAccountState;
+import org.ldaptive.control.PasswordPolicyControl;
+import org.ldaptive.jaas.LdapPrincipal;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+/** Unit test for {@link LDAPPrincipalSerializer}. */
+ at SuppressWarnings("javadoc")
+public class LDAPPrincipalSerializerTest {
+
+ LDAPPrincipalSerializer serializer;
+
+ @BeforeClass
+ public void setUp() {
+ serializer = new LDAPPrincipalSerializer();
+ }
+
+ @Test
+ public void testRoundTrip() throws IOException {
+ final LdapPrincipal p1 = new LdapPrincipal("alice", null);
+ final String s = serializer.serialize(p1);
+ Assert.assertEquals(s, "{\"LDAPN\":\"alice\"}");
+ Assert.assertTrue(serializer.supports(s));
+ final LdapPrincipal p2 = serializer.deserialize(s);
+ Assert.assertEquals(p1, p2);
+ assert p2 != null;
+ Assert.assertEquals(p2.getName(), "alice");
+ Assert.assertNull(p2.getLdapEntry());
+ Assert.assertNull(p2.getAccountState());
+ }
+
+ @Test
+ public void testRoundTripWithEntry() throws IOException {
+ final LdapEntry entry = new LdapEntry();
+ entry.setDn("uid=alice");
+ entry.addAttributes(new LdapAttribute("uid", "alice"), new LdapAttribute("givenName", "alice"));
+ final LdapPrincipal p1 = new LdapPrincipal("alice", entry);
+ final String s = serializer.serialize(p1);
+ Assert.assertEquals(
+ s,
+ "{\"LDAPN\":\"alice\",\"LDAPE\":{\"dn\":\"uid=alice\",\"uid\":[\"alice\"],\"givenName\":[\"alice\"]}}");
+ Assert.assertTrue(serializer.supports(s));
+ final LdapPrincipal p2 = serializer.deserialize(s);
+ Assert.assertEquals(p1, p2);
+ assert p2 != null;
+ Assert.assertEquals(p2.getName(), "alice");
+ Assert.assertEquals(p2.getLdapEntry(), entry);
+ Assert.assertNull(p2.getAccountState());
+ }
+
+ @Test
+ public void testSerializeWithEntryAndAccountStateError() throws IOException {
+ final LdapEntry entry = new LdapEntry();
+ entry.setDn("uid=alice");
+ entry.addAttributes(new LdapAttribute("uid", "alice"), new LdapAttribute("givenName", "alice"));
+ final PasswordPolicyAccountState accountState = new PasswordPolicyAccountState(
+ PasswordPolicyControl.Error.PASSWORD_EXPIRED);
+ final LdapPrincipal p1 = new LdapPrincipal("alice", entry, accountState);
+ final String s = serializer.serialize(p1);
+ Assert.assertEquals(
+ s,
+ "{" +
+ "\"LDAPN\":\"alice\"," +
+ "\"LDAPE\":{\"dn\":\"uid=alice\",\"uid\":[\"alice\"],\"givenName\":[\"alice\"]}," +
+ "\"LDAPAS\":" +
+ "{" +
+ "\"errors\":[{\"code\":0,\"msg\":\"PASSWORD_EXPIRED\"}]" +
+ "}" +
+ "}");
+ Assert.assertTrue(serializer.supports(s));
+ final LdapPrincipal p2 = serializer.deserialize(s);
+ Assert.assertEquals(p1, p2);
+ assert p2 != null;
+ Assert.assertEquals(p2.getName(), "alice");
+ Assert.assertEquals(p2.getLdapEntry(), entry);
+ Assert.assertNotNull(p2.getAccountState());
+ Assert.assertNull(p2.getAccountState().getWarning());
+ Assert.assertEquals(p2.getAccountState().getError().getCode(), 0);
+ Assert.assertEquals(p2.getAccountState().getError().getMessage(), "PASSWORD_EXPIRED");
+ }
+
+ @Test
+ public void testSerializeWithEntryAndAccountStateWarning() throws IOException {
+ final LdapEntry entry = new LdapEntry();
+ entry.setDn("uid=alice");
+ entry.addAttributes(new LdapAttribute("uid", "alice"), new LdapAttribute("givenName", "alice"));
+ final ZonedDateTime exp = ZonedDateTime.of(2025, 5, 25, 12, 35, 0, 0, ZoneId.of("UTC"));
+ final PasswordPolicyAccountState accountState = new PasswordPolicyAccountState(exp);
+ final LdapPrincipal p1 = new LdapPrincipal("alice", entry, accountState);
+ final String s = serializer.serialize(p1);
+ Assert.assertEquals(
+ s,
+ "{" +
+ "\"LDAPN\":\"alice\"," +
+ "\"LDAPE\":{\"dn\":\"uid=alice\",\"uid\":[\"alice\"],\"givenName\":[\"alice\"]}," +
+ "\"LDAPAS\":" +
+ "{" +
+ "\"warnings\":[{\"exp\":\"2025-05-25T12:35:00Z[UTC]\",\"loginsRemaining\":-1}]" +
+ "}" +
+ "}");
+ Assert.assertTrue(serializer.supports(s));
+ final LdapPrincipal p2 = serializer.deserialize(s);
+ Assert.assertEquals(p1, p2);
+ assert p2 != null;
+ Assert.assertEquals(p2.getName(), "alice");
+ Assert.assertEquals(p2.getLdapEntry(), entry);
+ Assert.assertNotNull(p2.getAccountState());
+ Assert.assertNull(p2.getAccountState().getError());
+ Assert.assertEquals(p2.getAccountState().getWarning().getExpiration(), exp);
+ Assert.assertEquals(p2.getAccountState().getWarning().getLoginsRemaining(), -1);
+ }
+
+ @Test
+ public void testSerializeWithEntryAndAccountStateAll() throws IOException {
+ final LdapEntry entry = new LdapEntry();
+ entry.setDn("uid=alice");
+ entry.addAttributes(new LdapAttribute("uid", "alice"), new LdapAttribute("givenName", "alice"));
+ final ZonedDateTime exp = ZonedDateTime.of(2025, 5, 25, 12, 35, 0, 0, ZoneId.of("UTC"));
+ final PasswordPolicyAccountState accountState = new PasswordPolicyAccountState(
+ exp,
+ PasswordPolicyControl.Error.PASSWORD_EXPIRED);
+ final LdapPrincipal p1 = new LdapPrincipal("alice", entry, accountState);
+ final String s = serializer.serialize(p1);
+ Assert.assertEquals(
+ s,
+ "{" +
+ "\"LDAPN\":\"alice\"," +
+ "\"LDAPE\":{\"dn\":\"uid=alice\",\"uid\":[\"alice\"],\"givenName\":[\"alice\"]}," +
+ "\"LDAPAS\":" +
+ "{" +
+ "\"warnings\":[{\"exp\":\"2025-05-25T12:35:00Z[UTC]\",\"loginsRemaining\":-1}]," +
+ "\"errors\":[{\"code\":0,\"msg\":\"PASSWORD_EXPIRED\"}]" +
+ "}" +
+ "}");
+ Assert.assertTrue(serializer.supports(s));
+ final LdapPrincipal p2 = serializer.deserialize(s);
+ Assert.assertEquals(p1, p2);
+ assert p2 != null;
+ Assert.assertEquals(p2.getName(), "alice");
+ Assert.assertEquals(p2.getLdapEntry(), entry);
+ Assert.assertNotNull(p2.getAccountState());
+ Assert.assertEquals(p2.getAccountState().getWarning().getExpiration(), exp);
+ Assert.assertEquals(p2.getAccountState().getWarning().getLoginsRemaining(), -1);
+ Assert.assertEquals(p2.getAccountState().getError().getCode(), 0);
+ Assert.assertEquals(p2.getAccountState().getError().getMessage(), "PASSWORD_EXPIRED");
+ }
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/test/resources/net/shibboleth/idp/authn/impl/LDAPAuthenticationAccountStateResult.json b/idp-authn-impl/src/test/resources/net/shibboleth/idp/authn/impl/LDAPAuthenticationAccountStateResult.json
new file mode 100644
index 000000000..6b4f67147
--- /dev/null
+++ b/idp-authn-impl/src/test/resources/net/shibboleth/idp/authn/impl/LDAPAuthenticationAccountStateResult.json
@@ -0,0 +1 @@
+{"id":"test","ts":1378827849463,"princ":[{"LDAPN":"bob","LDAPE":{"dn":"uid=1234,ou=people,dc=shibboleth,dc=net","cn":["Bob Cobb"],"givenName":["Bob","Robert"],"mail":["bob at shibboleth.net"],"sn":["Cobb"]},"LDAPAS":{"warnings":[{"loginsRemaining":5}]}}]}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list