Gary Wilson

September 3, 2007

Texas Python Regional Unconference

Filed under: Django, Python — Gary @ 10:47 am

Just a quick announcement for all the Houston and surrounding area Python lovers out there. There is a Texas Python Regional Unconference being held in a couple weeks (Sept. 15 - 16) at the University of Houston main campus. Registration is free and as simple as adding your name to the registration wiki page. I’ll be there to talk or hack Django with anyone interested. See you there!

August 26, 2006

Using CrackLib to require stronger passwords

Filed under: Django, Python — Gary @ 9:57 pm

Let’s face it, humans are not well adapted to memorizing strings of random characters; and hence, the average computer user is not very good at creating secure passwords. Most users create passwords made up of easy-to-remember words, like the name of a favorite sports team or maybe the name of a significant other. In this article I will show you how you can make Django require better passwords from your users.

Here are the packages you will need:

Installing the required packages

Step 1: Install CrackLib.
CrackLib is a library for checking that passwords are not easily crackable, or in other words, it makes sure that a password is not based on a simple character pattern or on a dictionary word. CrackLib is a common package that you should be able to find in your distribution’s package manager (or, quite possibly, it could already be installed). Alternatively, you could download the source and follow the installation instructions in the README file. By default, CrackLib installs a python package named cracklib, but it does not have as many features as python-crack. (Redhat does not include the cracklib python package in its cracklib package.)

Step 2 (optional, but recommended): Install extra word dictionaries.
Packaged with CrackLib is a file name cracklib-small. (Some distributions, like RedHat, don’t include this file in their cracklib package, in which case keep reading…) This file is a dictionary of words, simply a long list of words with one word per line. These 50,000 words are a good start, but we can do better. On the CrackLib download page, there is also a package named cracklib-words. After downloading and extracting the package, you will have a single file containing 1,648,379 “words”. Many distributions also have a cracklib-dicts or cracklib-words package that maybe the same or similar to the cracklib-words file on the CrackLib website.

For even more dictionaries, take a look at these word lists. It might also be a good idea to create your own list of words. For example, if you work for a company in the financial industry, it would be a good idea to make a list of words and slang specific to that industry. If you will be dealing with non-english speaking users, it would be a good idea to find some dictionaries in other languages.

Step 3: Create the word indexes that get used by CrackLib.
Now that we have our dictionaries, we need to compile them into an index that CrackLib uses. So, put all your dictionaries in one directory (/usr/share/dict/ is a common place). Depending on your distribution, there will be different commands to run. On gentoo (or if you installed from source), the following (as root) should do the trick:

create-cracklib-dict /usr/share/dict/*

And on RedHat:

mkdict /usr/share/dict/* | packer /usr/lib/cracklib_dict

These commands should create the following files:

/usr/lib/cracklib_dict.hwm
/usr/lib/cracklib_dict.pwd
/usr/lib/cracklib_dict.pwi

Step 4: Install python-crack
If your distribution’s package manager doesn’t include python-crack, then you will have to download and install it yourself. Neither Gentoo nor RedHat include this package, so I will walk you through the install. Download and unpack python-crack. Now, cd into the directory and run

./configure

By default, configure will setup python-crack’s files to be installed to /usr/local. This means that you will have to add /usr/local/lib/python2.4/site-packages to your PYTHONPATH. Alternatively, you could add a --prefix=/usr to the end of the configure command to install python-crack’s files to the usual /usr/lib/python2.4/site-packages directory.

If you get this error:

checking for prefix of the default cracklib dictionary database… unknown
configure: error: crack.h does not define CRACKLIB_DICTPATH. Please use DEFAULT_DICTPATH, see ./configure –help

then set the DEFAULT_DICTPATH variable when running configure, like so

./configure DEFAULT_DICTPATH=/usr/lib/cracklib_dict

Now run

make
make install

Now that we are finished installing, lets test out python-crack

$ python
Python 2.4.3 (#1, Jun 28 2006, 23:41:37)
[GCC 3.4.6 (Gentoo 3.4.6-r1, ssp-3.4.5-1.0, pie-8.7.9)] on linux2
Type “help”, “copyright”, “credits” or “license” for more information.
>>> import crack
>>> crack.VeryFascistCheck(’foo’)
[snip]
ValueError: it is WAY too short
>>> crack.VeryFascistCheck(’foobar’)
[snip]
ValueError: it is based on a dictionary word
>>> crack.VeryFascistCheck(’3#hsad2U>u2u’)
‘3#hsad2U>u2u’

VeryFascistCheck() raises a value error and prints an explaination if the password is not strong enough, otherwise it echos back the password.

Creating the Django Manipulator

from django import forms

class NewUserForm(forms.Manipulator):
    def __init__(self):
        self.fields = [
            forms.TextField(field_name="username", length=20, maxlength=20, is_required=True),
            forms.PasswordField(field_name="password", length=20, maxlength=50, is_required=True,
                validator_list=[isStrongPassword]),
            forms.PasswordField(field_name="confirm_password", length=20, maxlength=50, is_required=True,
                validator_list=[forms.validators.RequiredIfOtherFieldGiven("password", "You must confirm password"),
                                forms.validators.AlwaysMatchesOtherField("password", "Passwords did not match"),]),
        ]

def isStrongPassword(field_data, all_data):
    """Test the password with cracklib to make sure it is strong."""

    import crack
    # Increase the number of credits required from the default of 8 if you want.
    #crack.min_length = 11
    try:
        crack.VeryFascistCheck(field_data)
    except ValueError, message:
        raise forms.validators.ValidationError, "Password %s." % str(message)[3:]

Notice that I added isStrongPassword to the password field’s validator_list. Now you are ready to put the NewUserForm manipulator to use in your you views.py. After running the manipulator’s get_validation_errors(), bad passwords will generate errors like “Password is based on a dictionary word” or “Password does not contain enough DIFFERENT characters.” For help on using manipulators in your view, see the Forms, fields, and manipulators documentation.

If you want to make Django’s built-in authentication require stronger passwords, then you could add validator_list=[isStrongPassword] to the password field of django.contrib.auth.models.User.

Now, go forth and require strong passwords.

July 9, 2006

Cyclomatic complexity for python code

Filed under: Python — Gary @ 12:37 am

We all know that maintanence is the biggest cost of software. You can keep the maintanence cost of your program down by writing readable code. More readable code means faster bug fixing, which leads to more time for adding new bugs.

One measure of readability is cyclomatic complexity, a software metric that measures a program’s complexity based on the number of distinct paths or branches in the code. Carnegie Mellon’s Software Engineering Institute defines the following complexity risk levels:

Cyclomatic Complexity

Cyclomatic Complexity

Risk Evaluation

1-10

a simple program, without much risk

11-20

more complex, moderate risk

21-50

complex, high risk program

greater than 50

untestable program (very high risk)

I have written a python script that take these complexity scripts a bit further by printing the most complex functions/methods in an entire python package specified on the command line. Save the following as complexity.py

#!/usr/bin/python

"""
Find the most complex functions/methods in your python code.

This program is offered freely into the public domain by
Gary Wilson (gary.wilson@gmail.com).

This code requires the complexity shell scripts found at
http://journyx.com/curt/complexity.html
Download the four files and then specify the path to the
"complexity" script below, as PATH_TO_COMPLEXITY.

Note: To get the scripts to work for me I had to change the line
open (T,"|$dn/tab|sort -n +5") || die "no tab?";
to
open (T,"|$dn/tab|sort -n -k 6") || die "no tab?";
This line appears once in "complexdef" and twice in "complexdefclass".

Example uses:

Search the current directory.
python complexity.py

Search the mypythonpackage package.
python complexity.py ~/mypythonpackage

You can also specify multiple files and/or directories.
python complexity.py ~/mypythonpackage /src/myscript.py

This script will search for all files ending in ".py" within the
paths specified on the command line.  All functions and methods will
be sorted by decreasing complexity and printed along with the
filename and number of lines of code.

By default, this script will print out all functions/methods found.
If you would rather only see functions/methods with a certain
complexity or above, then change the COMPLEXITY_LEVEL setting below.
"""

## User Settings ###########################################
PATH_TO_COMPLEXITY = '~/complex/complexity'
# Only show functions/methods with a complexity greater than or
# equal to COMPLEXITY_LEVEL.  Setting to 0 will print all functions/methods.
COMPLEXITY_LEVEL = 0
############################################################

import sys
import re
import os
from subprocess import Popen, PIPE

def complexity_for_file(filename):
    cmd = '%s %s' % (PATH_TO_COMPLEXITY, filename)
    p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, close_fds=True)
    errors = p.stderr.read()
    if errors:
        sys.exit(errors)
    tuples = []
    reached_functions = False
    function_header = re.compile('Funcname|Classless_Function')
    for line in p.stdout.readlines():
        if function_header.match(line):
            reached_functions = True
        if reached_functions:
            fields = line.split()
            # We don't want the header lines.
            try:
                int(fields[5])
            except:
                continue
            # filename, function name, lines of code, and complexity.
            tuples.append((filename, fields[0], fields[4], fields[5]))
    return tuples

def add_to_functions(functions, dir, filenames):
    for filename in filenames:
        if filename.endswith('.py'):
            functions += complexity_for_file(os.path.join(dir, filename))

def get_column_widths(function_tuples):
    widths = []
    if not function_tuples:
        return widths
    for col in range(len(function_tuples[0])):
        widths.append(max([len(ft[col]) for ft in function_tuples]))
    return widths

def get_print_parms(widths, function_tuple):
    parms = []
    for x in range(len(widths)):
        parms.append(widths[x])
        parms.append(function_tuple[x])
    return tuple(parms)

def main():
    function_tuples = []
    paths = sys.argv[1:] or os.path.curdir
    for path in paths:
        path = os.path.expanduser(path)
        if os.path.isdir(path):
            os.path.walk(path, add_to_functions, function_tuples)
        elif os.path.isfile(path):
            function_tuples += complexity_for_file(path)
    # Filter out functions less than desired complexity level.
    function_tuples = [t for t in function_tuples if int(t[-1]) >= COMPLEXITY_LEVEL]
    # Sort by complexity.
    function_tuples.sort(key=lambda t: int(t[-1]))
    function_tuples.reverse()
    print "\nShowing functions/methods with complexity greater than or equal to %s:\n" % COMPLEXITY_LEVEL
    if function_tuples:
        headers = ('Filename', 'Function/Method', 'Lines of Code', 'Complexity')
        widths = get_column_widths(function_tuples + [headers])
        print '%-*s %-*s %*s %*s' % get_print_parms(widths, headers)
        for ft in function_tuples:
            print '%-*s %-*s %*s %*s' % get_print_parms(widths, ft)
    else:
        print "None."

if __name__ == "__main__":
    sys.exit(main())

Blog at WordPress.com.