Search code examples
pythonconfigcherrypy

Enabling digest authentication in Cherrypy server.conf


To enable digest auth in Cherrypy they say to use code like this:

from cherrypy.lib import auth_digest

USERS = {'jon': 'secret'}

conf = {
   '/protected/area': {
        'tools.auth_digest.on': True,
        'tools.auth_digest.realm': 'localhost',
        'tools.auth_digest.get_ha1': auth_digest.get_ha1_dict_plain(USERS),
        'tools.auth_digest.key': 'a565c27146791cfb'
   }
}

cherrypy.quickstart(myapp, '/', conf)

And it works pretty well. But I use server.conf file to store all the configs of my app and I want to continue of using this file. So I write there new section:

[/protected/area]
tools.auth_digest.on = True
tools.auth_digest.realm = 'localhost',
tools.auth_digest.get_ha1 = auth_digest.get_ha1_dict_plain({'jon': 'secret'}),
tools.auth_digest.key = 'a565c27146791cfb'

After thjis I've got errr:

ValueError: ('Config error in section: \'/protected/area\', option: \'tools.auth_digest.get_ha1\', value: "auth_digest.get_ha1_dict_plain({\'jon\': \'secret\'}),". Config values must be valid Python.', 'TypeError', ("unrepr could not resolve the name 'auth_digest'",))

I understand the reason but I dont know how to provide "valid Python" with server.conf. Help me, please.


Solution

  • You can make that function call in you application and use the resulting function in the config like:

    myapp/__init__.py:

    get_ha1 = auth_digest.get_ha1_dict_plain({'jon': 'secret'})
    

    server.conf:

    [/protected/area]
    tools.auth_digest.on = True
    tools.auth_digest.realm = 'localhost'
    tools.auth_digest.get_ha1 = myapp.get_ha1
    tools.auth_digest.key = 'a565c27146791cfb'
    

    The problem with that is that you're defining credentials in code.

    It might worth mention that you can use other functions no just the one that you define your users with plain texts passwords in a dict, you can use an htdigest file using the one from cherrypy.lib.auth_digest.get_ha1_file_htdigest or implement your own ha1 function like the one that the get_ha1_dict_plain returns:

    def get_ha1_dict_plain(user_password_dict):
        """Returns a get_ha1 function which obtains a plaintext password from a
        dictionary of the form: {username : password}.
        If you want a simple dictionary-based authentication scheme, with plaintext
        passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the
        get_ha1 argument to digest_auth().
        """
        def get_ha1(realm, username):
            password = user_password_dict.get(username)
            if password:
                return md5_hex('%s:%s:%s' % (username, realm, password))
            return None
    
        return get_ha1
    

    I implemented one that gets the ha1 from the database, for example using this sqlalchemy model (https://github.com/cyraxjoe/maki/blob/master/maki/db/models.py#L174-L189):

    class User(Base):
        __tablename__ = 'users'
    
        name   = Column(String(32), unique=True, nullable=False)
        vname  = Column(String(64))
        email  = Column(String(64), nullable=False)
        ha1    = Column(String(32), nullable=False)
        active = Column(Boolean, server_default='True')
    
    
        @validates('ha1')
        def validates_ha1(self, key, passwd):
            if self.name is None:
                raise Exception('Set the name first')
            pack = ':'.join([self.name, maki.constants.REALM, passwd])
            return hashlib.md5(pack.encode()).hexdigest()
    

    And a get_ha1 function (https://github.com/cyraxjoe/maki/blob/master/maki/db/utils.py#L63):

    def get_user_ha1(realm, username):
        # realm is not used the stored hash already used it.
        user = db.ses.query(db.models.User).filter_by(name=username).scalar()
        if user is not None:
            return user.ha1
    

    The important part is that a ha1 is only an md5 hash of "user:real:password", you can implement that in a lot of different places.