Sfoglia il codice sorgente

move and rename password encoding classes.
change saltSource arument to salt argument, which impl may or may not use.

Colin Sampaleanu 21 anni fa
parent
commit
3d089aaa67

+ 3 - 1
core/src/main/java/org/acegisecurity/providers/dao/DaoAuthenticationProvider.java

@@ -22,6 +22,7 @@ import net.sf.acegisecurity.BadCredentialsException;
 import net.sf.acegisecurity.DisabledException;
 import net.sf.acegisecurity.DisabledException;
 import net.sf.acegisecurity.providers.AuthenticationProvider;
 import net.sf.acegisecurity.providers.AuthenticationProvider;
 import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
 import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import net.sf.acegisecurity.providers.encoding.*;
 
 
 import org.springframework.beans.factory.InitializingBean;
 import org.springframework.beans.factory.InitializingBean;
 
 
@@ -95,8 +96,9 @@ public class DaoAuthenticationProvider implements AuthenticationProvider,
         }
         }
 
 
         if (!passwordEncoder.isPasswordValid(user.getPassword(),
         if (!passwordEncoder.isPasswordValid(user.getPassword(),
-                authentication.getCredentials().toString(), user))
+                authentication.getCredentials().toString(), user)) {
             throw new BadCredentialsException("Bad credentials presented");
             throw new BadCredentialsException("Bad credentials presented");
+        }
 
 
         if (!user.isEnabled()) {
         if (!user.isEnabled()) {
             throw new DisabledException("User is disabled");
             throw new DisabledException("User is disabled");

+ 50 - 0
core/src/main/java/org/acegisecurity/providers/encoding/BaseDigestPasswordEncoder.java

@@ -0,0 +1,50 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * 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.sf.acegisecurity.providers.encoding;
+
+import net.sf.acegisecurity.providers.encoding.*;
+
+
+/**
+ * <p>
+ * Convenience base for Digest password encoders
+ * </p>
+ *
+ * @author colin sampaleanu
+ * @version $Id$
+ */
+public abstract class BaseDigestPasswordEncoder implements PasswordEncoder {
+    //~ Instance fields ========================================================
+
+    private boolean encodeHashAsBase64 = false;
+
+    //~ Methods ================================================================
+
+    /**
+     * The encoded password is normally returned as Hex (32 char) version of
+     * the hash bytes. Setting this property to true will cause the encoded
+     * pass to be returned as Base64 text, which will consume 24 characters.
+     *
+     * @param encodeHashAsBase64 DOCUMENT ME!
+     */
+    public void setEncodeHashAsBase64(boolean encodeHashAsBase64) {
+        this.encodeHashAsBase64 = encodeHashAsBase64;
+    }
+
+    public boolean getEncodeHashAsBase64() {
+        return encodeHashAsBase64;
+    }
+}

+ 64 - 0
core/src/main/java/org/acegisecurity/providers/encoding/Md5PasswordEncoder.java

@@ -0,0 +1,64 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * 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.sf.acegisecurity.providers.encoding;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+
+
+/**
+ * <p>
+ * MD5 implementation of PasswordEncoder.
+ * </p>
+ * 
+ * <p>
+ * A null password is encoded to the same value as an empty ("") password.
+ * </p>
+ *
+ * @author colin sampaleanu
+ * @version $Id$
+ */
+public class Md5PasswordEncoder extends BaseDigestPasswordEncoder
+    implements PasswordEncoder {
+    //~ Methods ================================================================
+
+    /* (non-Javadoc)
+     * @see net.sf.acegisecurity.providers.dao.PasswordEncoder#isPasswordValid(java.lang.String, java.lang.String, java.lang.Object)
+     */
+    public boolean isPasswordValid(String encPass, String rawPass, Object salt) {
+        String pass1 = "" + encPass;
+        String pass2 = encodeInternal("" + rawPass);
+
+        return pass1.equals(pass2);
+    }
+
+    /* (non-Javadoc)
+     * @see net.sf.acegisecurity.providers.dao.PasswordEncoder#encodePassword(java.lang.String, java.lang.Object)
+     */
+    public String encodePassword(String rawPass, Object salt) {
+        return encodeInternal("" + rawPass);
+    }
+
+    private String encodeInternal(String input) {
+        if (!getEncodeHashAsBase64()) {
+            return DigestUtils.md5Hex(input);
+        }
+
+        byte[] encoded = Base64.encodeBase64(DigestUtils.md5(input));
+
+        return new String(encoded);
+    }
+}

+ 87 - 0
core/src/main/java/org/acegisecurity/providers/encoding/PasswordEncoder.java

@@ -0,0 +1,87 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * 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.sf.acegisecurity.providers.encoding;
+
+import org.springframework.dao.DataAccessException;
+
+
+/**
+ * <p>
+ * Interface for performing authentication operations on a password, so that
+ * digest algorithms may be abstracted.
+ * </p>
+ *
+ * @author colin sampaleanu
+ * @version $Id$
+ */
+public interface PasswordEncoder {
+    //~ Methods ================================================================
+
+    /**
+     * <p>
+     * Validates a specified 'raw' password against an encoded password
+     * previously returned form {@link #encodePassword(String, Object)}. The
+     * raw password will first be encoded, and then both values will be
+     * compared.
+     * </p>
+     * 
+     * <p>
+     * The specified salt will potentially be used by the implementation to
+     * 'salt' the initial value before encoding. If a salt value is provided,
+     * it must be the same as the value used when calling {@link
+     * #encodePassword(String, Object)} to produce the first encoded value.
+     * Note that a specific implementation may choose to ignore the salt
+     * value, or provide its own.
+     * </p>
+     *
+     * @param encPass a pre-encoded password
+     * @param rawPass a raw password to encode and compare against the
+     *        pre-encoded password
+     * @param an object optionally used by the implementation to 'salt' the raw
+     *        password before encoding. A null value is legal.
+     *
+     * @return DOCUMENT ME!
+     */
+    public boolean isPasswordValid(String encPass, String rawPass,
+        Object saltSource) throws DataAccessException;
+
+    /**
+     * <p>
+     * Encodes the specified raw password with an implementation specific
+     * algorithm. This will generally be a one-way message digest such as MD5
+     * or SHA, but may also be a plaintext variant which does no encoding at
+     * all, but rather returns the same password it was fed. The latter is
+     * useful to plug in when the original password must be stored as-is.
+     * </p>
+     * 
+     * <p>
+     * The specified salt will potentially be used by the implementation to
+     * 'salt' the initial value before encoding, in order to prevent
+     * dictionary attacks. If a salt value is provided, the same salt value
+     * must be use when calling the  {@link #isPasswordValid(String, String,
+     * Object)} function. Note that a specific implementation may choose to
+     * ignore the salt value, or provide its own.
+     * </p>
+     *
+     * @param rawPass the password to encode
+     * @param an object optionally used by the implementation to 'salt' the raw
+     *        password before encoding. A null value is legal.
+     *
+     * @return DOCUMENT ME!
+     */
+    public String encodePassword(String rawPass, Object salt)
+        throws DataAccessException;
+}

+ 68 - 0
core/src/main/java/org/acegisecurity/providers/encoding/PlaintextPasswordEncoder.java

@@ -0,0 +1,68 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * 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.sf.acegisecurity.providers.encoding;
+
+/**
+ * <p>
+ * Plaintext implementation of PasswordEncoder.
+ * </p>
+ *
+ * @author colin sampaleanu
+ * @version $Id$
+ */
+public class PlaintextPasswordEncoder implements PasswordEncoder {
+    //~ Instance fields ========================================================
+
+    private boolean ignorePasswordCase = false;
+
+    //~ Methods ================================================================
+
+    /**
+     * Indicates whether the password comparison is case sensitive. Defaults to
+     * <code>false</code>, meaning an exact case match is required.
+     *
+     * @param ignorePasswordCase set to <code>true</code> for less stringent
+     *        comparison
+     */
+    public void setIgnorePasswordCase(boolean ignorePasswordCase) {
+        this.ignorePasswordCase = ignorePasswordCase;
+    }
+
+    public boolean isIgnorePasswordCase() {
+        return ignorePasswordCase;
+    }
+
+    /* (non-Javadoc)
+     * @see net.sf.acegisecurity.providers.dao.PasswordEncoder#isPasswordValid(java.lang.String, java.lang.String, java.lang.Object)
+     */
+    public boolean isPasswordValid(String encPass, String rawPass, Object salt) {
+        String pass1 = "" + encPass;
+        String pass2 = "" + rawPass;
+
+        if (!ignorePasswordCase) {
+            return pass1.equals(pass2);
+        } else {
+            return pass1.equalsIgnoreCase(pass2);
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see net.sf.acegisecurity.providers.dao.PasswordEncoder#encodePassword(java.lang.String, java.lang.Object)
+     */
+    public String encodePassword(String rawPass, Object salt) {
+        return rawPass;
+    }
+}

+ 64 - 0
core/src/main/java/org/acegisecurity/providers/encoding/ShaPasswordEncoder.java

@@ -0,0 +1,64 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * 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.sf.acegisecurity.providers.encoding;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+
+
+/**
+ * <p>
+ * SHA implementation of PasswordEncoder.
+ * </p>
+ * 
+ * <p>
+ * A null password is encoded to the same value as an empty ("") password.
+ * </p>
+ *
+ * @author colin sampaleanu
+ * @version $Id$
+ */
+public class ShaPasswordEncoder extends BaseDigestPasswordEncoder
+    implements PasswordEncoder {
+    //~ Methods ================================================================
+
+    /* (non-Javadoc)
+     * @see net.sf.acegisecurity.providers.dao.PasswordEncoder#isPasswordValid(java.lang.String, java.lang.String, java.lang.Object)
+     */
+    public boolean isPasswordValid(String encPass, String rawPass, Object salt) {
+        String pass1 = "" + encPass;
+        String pass2 = encodeInternal("" + rawPass);
+
+        return pass1.equals(pass2);
+    }
+
+    /* (non-Javadoc)
+     * @see net.sf.acegisecurity.providers.dao.PasswordEncoder#encodePassword(java.lang.String, java.lang.Object)
+     */
+    public String encodePassword(String rawPass, Object salt) {
+        return encodeInternal("" + rawPass);
+    }
+
+    private String encodeInternal(String input) {
+        if (!getEncodeHashAsBase64()) {
+            return DigestUtils.shaHex(input);
+        }
+
+        byte[] encoded = Base64.encodeBase64(DigestUtils.sha(input));
+
+        return new String(encoded);
+    }
+}

+ 3 - 3
core/src/main/java/org/acegisecurity/userdetails/UserDetailsService.java

@@ -36,9 +36,9 @@ public interface AuthenticationDao {
     /**
     /**
      * Locates the user based on the username. In the actual implementation,
      * Locates the user based on the username. In the actual implementation,
      * the search may possibly be case insensitive, or case insensitive
      * the search may possibly be case insensitive, or case insensitive
-     * depending on how the implementaion instance is configured. In this case,
-     * the User object that comes back may have a username that is of a different
-     * case than what was actually requested..
+     * depending on how the implementaion instance is configured. In this
+     * case, the User object that comes back may have a username that is of a
+     * different case than what was actually requested..
      *
      *
      * @param username the username presented to the {@link
      * @param username the username presented to the {@link
      *        DaoAuthenticationProvider}
      *        DaoAuthenticationProvider}

+ 0 - 1
core/src/test/java/org/acegisecurity/providers/dao/DaoAuthenticationProviderTests.java

@@ -37,7 +37,6 @@ import org.springframework.dao.DataRetrievalFailureException;
  * @version $Id$
  * @version $Id$
  */
  */
 public class DaoAuthenticationProviderTests extends TestCase {
 public class DaoAuthenticationProviderTests extends TestCase {
-
     //~ Methods ================================================================
     //~ Methods ================================================================
 
 
     public final void setUp() throws Exception {
     public final void setUp() throws Exception {

+ 20 - 17
core/src/test/java/org/acegisecurity/providers/dao/MD5PasswordEncoderTest.java

@@ -12,10 +12,14 @@
  * See the License for the specific language governing permissions and
  * See the License for the specific language governing permissions and
  * limitations under the License.
  * limitations under the License.
  */
  */
+
 package net.sf.acegisecurity.providers.dao;
 package net.sf.acegisecurity.providers.dao;
 
 
 import junit.framework.TestCase;
 import junit.framework.TestCase;
 
 
+import net.sf.acegisecurity.providers.encoding.*;
+
+
 /**
 /**
  * <p>
  * <p>
  * TestCase for PlaintextPasswordEncoder.
  * TestCase for PlaintextPasswordEncoder.
@@ -25,23 +29,22 @@ import junit.framework.TestCase;
  * @version $Id$
  * @version $Id$
  */
  */
 public class MD5PasswordEncoderTest extends TestCase {
 public class MD5PasswordEncoderTest extends TestCase {
+    //~ Methods ================================================================
 
 
-  public void testBasicFunctionality() {
-    
-    MD5PasswordEncoder pe = new MD5PasswordEncoder();
-    String raw = "abc123";
-    String badRaw = "abc321";
-    String encoded = pe.encodePassword(raw, null);   // no SALT source
-    assertTrue(pe.isPasswordValid(encoded, raw, null));
-    assertFalse(pe.isPasswordValid(encoded, badRaw, null));
-    assertTrue(encoded.length() == 32);
-    
-    // now try Base64
-    pe.setEncodeHashAsBase64(true);
-    encoded = pe.encodePassword(raw, null);   // no SALT source
-    assertTrue(pe.isPasswordValid(encoded, raw, null));
-    assertFalse(pe.isPasswordValid(encoded, badRaw, null));
-    assertTrue(encoded.length() != 32);
-  }
+    public void testBasicFunctionality() {
+        Md5PasswordEncoder pe = new Md5PasswordEncoder();
+        String raw = "abc123";
+        String badRaw = "abc321";
+        String encoded = pe.encodePassword(raw, null); // no SALT source
+        assertTrue(pe.isPasswordValid(encoded, raw, null));
+        assertFalse(pe.isPasswordValid(encoded, badRaw, null));
+        assertTrue(encoded.length() == 32);
 
 
+        // now try Base64
+        pe.setEncodeHashAsBase64(true);
+        encoded = pe.encodePassword(raw, null); // no SALT source
+        assertTrue(pe.isPasswordValid(encoded, raw, null));
+        assertFalse(pe.isPasswordValid(encoded, badRaw, null));
+        assertTrue(encoded.length() != 32);
+    }
 }
 }

+ 36 - 31
core/src/test/java/org/acegisecurity/providers/dao/PlaintextPasswordEncoderTest.java

@@ -12,10 +12,14 @@
  * See the License for the specific language governing permissions and
  * See the License for the specific language governing permissions and
  * limitations under the License.
  * limitations under the License.
  */
  */
+
 package net.sf.acegisecurity.providers.dao;
 package net.sf.acegisecurity.providers.dao;
 
 
 import junit.framework.TestCase;
 import junit.framework.TestCase;
 
 
+import net.sf.acegisecurity.providers.encoding.*;
+
+
 /**
 /**
  * <p>
  * <p>
  * TestCase for PlaintextPasswordEncoder.
  * TestCase for PlaintextPasswordEncoder.
@@ -25,35 +29,36 @@ import junit.framework.TestCase;
  * @version $Id$
  * @version $Id$
  */
  */
 public class PlaintextPasswordEncoderTest extends TestCase {
 public class PlaintextPasswordEncoderTest extends TestCase {
+    //~ Methods ================================================================
+
+    public void testBasicFunctionality() {
+        PlaintextPasswordEncoder pe = new PlaintextPasswordEncoder();
+
+        String raw = "abc123";
+        String rawDiffCase = "AbC123";
+        String badRaw = "abc321";
+
+        // should be able to validate even without encoding
+        String encoded = raw;
+        assertTrue(pe.isPasswordValid(encoded, raw, null)); // no SALT source
+        assertFalse(pe.isPasswordValid(encoded, badRaw, null));
+
+        // now make sure encoded version it gives us back is comparable as well
+        encoded = pe.encodePassword(raw, null);
+        assertTrue(pe.isPasswordValid(encoded, raw, null)); // no SALT source
+        assertFalse(pe.isPasswordValid(encoded, badRaw, null));
+
+        // make sure default is not to ignore password case
+        encoded = pe.encodePassword(rawDiffCase, null);
+        assertFalse(pe.isPasswordValid(encoded, raw, null));
+
+        // now check for ignore password case
+        pe = new PlaintextPasswordEncoder();
+        pe.setIgnorePasswordCase(true);
 
 
-  public void testBasicFunctionality() {
-    PlaintextPasswordEncoder pe = new PlaintextPasswordEncoder();
-
-    String raw = "abc123";
-    String rawDiffCase = "AbC123";
-    String badRaw = "abc321";
-    // should be able to validate even without encoding
-    String encoded = raw;
-    assertTrue(pe.isPasswordValid(encoded, raw, null));   // no SALT source
-    assertFalse(pe.isPasswordValid(encoded, badRaw, null));
-
-    // now make sure encoded version it gives us back is comparable as well
-    encoded = pe.encodePassword(raw, null);
-    assertTrue(pe.isPasswordValid(encoded, raw, null));   // no SALT source
-    assertFalse(pe.isPasswordValid(encoded, badRaw, null));
-    
-    // make sure default is not to ignore password case
-    encoded = pe.encodePassword(rawDiffCase, null);
-    assertFalse(pe.isPasswordValid(encoded, raw, null));
-    
-    // now check for ignore password case
-    pe = new PlaintextPasswordEncoder();
-    pe.setIgnorePasswordCase(true);
-
-    // should be able to validate even without encoding
-    encoded = pe.encodePassword(rawDiffCase, null);
-    assertTrue(pe.isPasswordValid(encoded, raw, null));
-    assertFalse(pe.isPasswordValid(encoded, badRaw, null));
-  }
-
-}
+        // should be able to validate even without encoding
+        encoded = pe.encodePassword(rawDiffCase, null);
+        assertTrue(pe.isPasswordValid(encoded, raw, null));
+        assertFalse(pe.isPasswordValid(encoded, badRaw, null));
+    }
+}

+ 22 - 19
core/src/test/java/org/acegisecurity/providers/dao/SHAPasswordEncoderTest.java

@@ -12,36 +12,39 @@
  * See the License for the specific language governing permissions and
  * See the License for the specific language governing permissions and
  * limitations under the License.
  * limitations under the License.
  */
  */
+
 package net.sf.acegisecurity.providers.dao;
 package net.sf.acegisecurity.providers.dao;
 
 
 import junit.framework.TestCase;
 import junit.framework.TestCase;
 
 
+import net.sf.acegisecurity.providers.encoding.*;
+
+
 /**
 /**
  * <p>
  * <p>
- * TestCase for SHAPasswordEncoder.
+ * TestCase for ShaPasswordEncoder.
  * </p>
  * </p>
  *
  *
  * @author colin sampaleanu
  * @author colin sampaleanu
  * @version $Id$
  * @version $Id$
  */
  */
 public class SHAPasswordEncoderTest extends TestCase {
 public class SHAPasswordEncoderTest extends TestCase {
+    //~ Methods ================================================================
+
+    public void testBasicFunctionality() {
+        ShaPasswordEncoder pe = new ShaPasswordEncoder();
+        String raw = "abc123";
+        String badRaw = "abc321";
+        String encoded = pe.encodePassword(raw, null); // no SALT source
+        assertTrue(pe.isPasswordValid(encoded, raw, null));
+        assertFalse(pe.isPasswordValid(encoded, badRaw, null));
+        assertTrue(encoded.length() == 40);
 
 
-  public void testBasicFunctionality() {
-    
-    SHAPasswordEncoder pe = new SHAPasswordEncoder();
-    String raw = "abc123";
-    String badRaw = "abc321";
-    String encoded = pe.encodePassword(raw, null);   // no SALT source
-    assertTrue(pe.isPasswordValid(encoded, raw, null));
-    assertFalse(pe.isPasswordValid(encoded, badRaw, null));
-    assertTrue(encoded.length() == 40);
-    
-    // now try Base64
-    pe.setEncodeHashAsBase64(true);
-    encoded = pe.encodePassword(raw, null);   // no SALT source
-    assertTrue(pe.isPasswordValid(encoded, raw, null));
-    assertFalse(pe.isPasswordValid(encoded, badRaw, null));
-    assertTrue(encoded.length() != 40);
-    
-  }
+        // now try Base64
+        pe.setEncodeHashAsBase64(true);
+        encoded = pe.encodePassword(raw, null); // no SALT source
+        assertTrue(pe.isPasswordValid(encoded, raw, null));
+        assertFalse(pe.isPasswordValid(encoded, badRaw, null));
+        assertTrue(encoded.length() != 40);
+    }
 }
 }

+ 1 - 1
samples/contacts/src/main/java/sample/contact/ClientApplication.java

@@ -30,7 +30,7 @@ import java.util.Map;
 
 
 /**
 /**
  * Demonstrates accessing the {@link ContactManager} via remoting protocols.
  * Demonstrates accessing the {@link ContactManager} via remoting protocols.
- * 
+ *
  * <P>
  * <P>
  * Based on Spring's JPetStore sample, written by Juergen Hoeller.
  * Based on Spring's JPetStore sample, written by Juergen Hoeller.
  * </p>
  * </p>

+ 1 - 1
test/acegisecuritytest.properties

@@ -1,5 +1,5 @@
 #HSQL database
 #HSQL database
-#Thu Apr 15 12:20:59 EDT 2004
+#Thu Apr 15 23:43:47 EDT 2004
 sql.strict_fk=true
 sql.strict_fk=true
 readonly=false
 readonly=false
 sql.strong_fk=true
 sql.strong_fk=true