Skip to content

Commit

Permalink
Merge pull request #26 from freeipa/drop_captcha_key
Browse files Browse the repository at this point in the history
Drop captcha key_location and auto-create key file
  • Loading branch information
tiran committed Aug 19, 2015
2 parents b9df8d7 + ee39b95 commit 9de324d
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 103 deletions.
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,4 @@ __pycache__

# testing infrastrucutre
/.tox
/resets.db
/captcha.db
/key
/var
14 changes: 7 additions & 7 deletions docs/deploy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The CAPTCHA functionality relies on the Pillow library::
dnf install python-pillow

These components are the core application. CherryPy as the web framework,
Jinja2 provides templating, and SQLAlchemy is used for the databases::
Jinja2 provides templating, and SQLAlchemy is used for the database::

dnf install python-cherrypy python-jinja2 python-sqlalchemy

Expand Down Expand Up @@ -83,16 +83,16 @@ Next, the installer copies the apache config from the conf directory to
installation of the portal, you probably will not need this file, because you
probably know what you're doing.

Then, the installer creates the directory where the portal keeps its databases::
Then, the installer creates the directory where the portal keeps its database::

mkdir -p /var/lib/freeipa_community_portal
mkdir -p -m 750 /var/lib/freeipa_community_portal
chown apache:apache /var/lib/freeipa_community_portal/

If Apache doesn't own this folder, it will vomit when attempting to put
databases in it. Next, the installer generates a random key and stores it in a
file called "key" the above directory. The portal uses this key to secure the
captcha. It would be mostly harmless if this key gets compromised, so there's
no need to take any special precautions to secure it.
database in it. Next, the installer generates a random key and stores it in a
file called "captcha.key" the above directory. The portal uses this key to
secure the captcha. It would be mostly harmless if this key gets compromised,
so there's no need to take any special precautions to secure it.

After this, the installer does::

Expand Down
31 changes: 11 additions & 20 deletions docs/development.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,21 @@ and then do::
in the root of the tree. This should install a local, editable copy of the app,
and put all of the configuration files and assets where they are expected.

You will also have to create a key file for the captcha. Because this is
development, you can probably just do::

touch key

and the empty file will work. There just needs to be a key file available to
read.

You can configure exactly where the application spews its files by editing the
freeipa_community_portal_dev.ini file and plugging in values that make you
happy.
freeipa_community_portal_dev.ini file in freeipa_community_portal/conf and
plugging in values that make you happy. By default the development server uses
var/ in your current working directory to store its database and captcha key
file. The directory, sqlite database and key files are created automatically.

Before you run the app, even in tree, you should kinit as a user with
sufficient permissions as outlined in the deployment doc.
sufficient permissions as outlined in the deployment doc. You can also drop
a client keytab in your var/ directory.

To run the application in-tree, do::

python freeipa_community_portal/app.py

If you're running an IPA server on the host you're doing development on, one of
the IPA apps already uses port 8080 (the default CherryPy port). You may need
to add::

cherrypy.config.update({"server.socket_port": 8099})
python -m freeipa_community_portal

to app.py between lines 182 and 183. You can use any port that isn't being
used be default, not just 8099.
On an IPA server Dogtag PKI is already occupying port 8080. For that reason
the development server listens on port 10080 on localhost. You can change
the port in freeipa_community_port/__main__.py if the port is already used
on your machine.
43 changes: 43 additions & 0 deletions freeipa_community_portal/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Authors:
# Christian Heimes <[email protected]>
#
# Copyright (C) 2015 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Entry point for development server
Copy your keytab to ./var/portal.keytab and run::
python2.7 -m freeipa_community_portal
"""
import cherrypy

from freeipa_community_portal import app
from freeipa_community_portal.config import config

config.load(config.development_config)

# 8080 is occupied by Dogtag
cherrypy.config.update({
'server.socket_host': '127.0.0.1',
'server.socket_port': 10080,
})

cherrypy.quickstart(
app.app(),
'/',
app.conf
)
12 changes: 7 additions & 5 deletions freeipa_community_portal/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from freeipa_community_portal.model.password_reset import PasswordReset
# TODO: move over to a "from" import
import freeipa_community_portal.model.captcha_wrapper as captcha_helper
from freeipa_community_portal.config import config

TEMPLATE_ENV = jinja2.Environment(loader=jinja2.PackageLoader('freeipa_community_portal','templates'))

Expand Down Expand Up @@ -149,6 +150,8 @@ def check_captcha(args):
return "Incorrect Captcha response"
else:
return None


conf = {
'/assets': {
'tools.staticdir.on': True,
Expand All @@ -169,17 +172,16 @@ def check_captcha(args):
}
}


def app():
"""Main entry point for the web application. If you run this library as a
standalone application, you can just use this function
"""
if not config:
raise ValueError('Run config.load(configfile) first!')

webapp = SelfServicePortal()
webapp.user = SelfServiceUserRegistration() # pylint: disable=attribute-defined-outside-init
webapp.user = SelfServiceUserRegistration() # pylint: disable=attribute-defined-outside-init
webapp.request_reset = RequestSelfServicePasswordReset()
webapp.reset_password = SelfServicePasswordReset()
return webapp

if __name__ == "__main__":
webapp = app()
cherrypy.quickstart(webapp, '/', conf)
4 changes: 0 additions & 4 deletions freeipa_community_portal/conf/freeipa_community_portal.ini
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ [email protected]
# the address to send admin mail to
default_admin_email[email protected]

[Captcha]
# the location of the key
key_location=/var/lib/freeipa_community_portal/key

[Database]
# the directory where we store our databases
db_directory=/var/lib/freeipa_community_portal/
Expand Down
10 changes: 2 additions & 8 deletions freeipa_community_portal/conf/freeipa_community_portal_dev.ini
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,12 @@ [email protected]
# the address to send admin mail to
default_admin_email[email protected]

[Captcha]

# the location of the key
key_location=key

[Database]

# the directory where we store our databases
db_directory=
db_directory=var/

[KRB5]
# set KRB5_CLIENT_KTNAME if non-empty
client_keytab=portal.keytab
client_keytab=var/portal.keytab
# set KRB5CCNAME if non-empty
ccache_name=
93 changes: 71 additions & 22 deletions freeipa_community_portal/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,74 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import ConfigParser
import errno
import os

from sqlalchemy import MetaData, create_engine


class Config(object):
default_configs = [
'/etc/freeipa_community_portal.ini',
'conf/freeipa_community_portal_dev.ini'
]
development_config = 'freeipa_community_portal/conf/freeipa_community_portal_dev.ini'
deployment_config = '/etc/freeipa_community_portal.ini'

captcha_length = 4
umask = 0o027

metadata = MetaData()

def __init__(self, *configs):
if not configs:
configs = self.default_configs
self._cfg = ConfigParser.SafeConfigParser()
print configs
self._cfg.read(configs)
def __init__(self):
self._cfg = None
self.configfile = None
self._captcha_key = None
self._engine = None

def __nonzero__(self):
return self._cfg is not None

def load(self, configfile):
cfg = ConfigParser.SafeConfigParser()
with open(configfile) as f:
cfg.readfp(f, configfile)
self._cfg = cfg
self.configfile = configfile
# set secure umask
os.umask(self.umask)
self._init_vardir()
self._init_captcha_key()
self._init_engine()

def _init_vardir(self):
"""Create our var directory with secure mode
"""
if not os.path.isdir(self.db_directory):
os.makedirs(self.db_directory, mode=0o750)

def _init_captcha_key(self):
"""Read or create captcha key file
"""
try:
with open(self.captcha_key_location, 'rb') as f:
self._captcha_key = f.read()
except IOError as e:
if e.errno != errno.ENOENT:
raise
new_key = os.urandom(8)
# write key with secure mode
with open(self.captcha_key_location, 'wb') as f:
os.fchmod(f.fileno(), 0o600)
f.write(new_key)
os.fdatasync(f.fileno())
# re-read key from file system in case somebody else wrote to it.
with open(self.captcha_key_location, 'rb') as f:
self._captcha_key = f.read()

def _init_engine(self):
"""Create engine and tables
"""
if self._engine is not None:
self._engine.close()
self._engine = create_engine('sqlite:///' + self.communityportal_db)
self.metadata.create_all(self._engine)

def _get_default(self, section, option, raw=False, vars=None,
default=None):
Expand All @@ -45,26 +95,25 @@ def _get_default(self, section, option, raw=False, vars=None,
return default

@property
def captcha_db(self):
return os.path.join(self._cfg.get('Database', 'db_directory'),
'captcha.db')
def engine(self):
return self._engine

@property
def db_directory(self):
return self._cfg.get('Database', 'db_directory')

@property
def communityportal_db(self):
return os.path.join(self.db_directory, 'communityportal.db')

@property
def captcha_key_location(self):
return self._cfg.get('Captcha', 'key_location')
return os.path.join(self.db_directory, 'captcha.key')

@property
def captcha_key(self):
if self._captcha_key is None:
with open(self.captcha_key_location, 'rb') as f:
self._captcha_key = f.read()
return self._captcha_key

@property
def reset_db(self):
return os.path.join(self._cfg.get('Database', 'db_directory'),
'resets.db')

@property
def smtp_server(self):
return self._cfg.get('Mailers', 'smtp_server')
Expand Down
9 changes: 8 additions & 1 deletion freeipa_community_portal/freeipa_community_portal.wsgi
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,12 @@

import cherrypy
from freeipa_community_portal import app
from freeipa_community_portal.config import config

application = cherrypy.Application(app.app(), script_name=None, config=app.conf)
config.load(config.deployment_config)

application = cherrypy.Application(
app.app(),
script_name=None,
config=app.conf
)
19 changes: 7 additions & 12 deletions freeipa_community_portal/model/captcha_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,18 @@
import base64
import hmac

from sqlalchemy import Table, Column, MetaData, String, DateTime, create_engine
from sqlalchemy import Table, Column, String, DateTime
from sqlalchemy.sql import select, insert, delete

from ..config import config

# retrieve the captcha key from the key file
# trust me, i know cryptography
LENGTH = config.captcha_length
KEY = config.captcha_key

_engine = create_engine('sqlite:///' + config.captcha_db)
_metadata = MetaData()
_captcha = Table('captcha', _metadata,
_captcha = Table('captcha', config.metadata,
Column('hmac', String, primary_key=True),
Column('timestamp', DateTime)
)
_metadata.create_all(_engine)


class CaptchaHelper(object):
Expand All @@ -54,12 +49,12 @@ def __init__(self):
"""create a new captcha """
# generate a captcha solution, which consists of 4 letter and digits
self.solution = u''.join(random.SystemRandom().choice(
(string.ascii_uppercase + string.digits).translate(None, '0OQ')) for _ in range(LENGTH)
(string.ascii_uppercase + string.digits).translate(None, '0OQ')) for _ in range(config.captcha_length)
)

# generate the captcha image, hold it as bytes
self.image = self.image_generator.generate(self.solution, format='jpeg').getvalue()
conn = _engine.connect()
conn = config.engine.connect()
conn.execute(
_captcha.insert().values(
hmac=self.solution_hash(),
Expand All @@ -78,14 +73,14 @@ def datauri(self):
def solution_hash(self):
"""combines the captcha solution and a secret key into a hash that can
be used to prove that a correct answer has been found"""
return hmac.new(KEY, self.solution).hexdigest()
return hmac.new(config.captcha_key, self.solution).hexdigest()

def checkResponse(response, solution):
"""Compares a given solution hash with the response provided"""
valid = False
digest = hmac.new(KEY, response.upper()).hexdigest()
digest = hmac.new(config.captcha_key, response.upper()).hexdigest()
if hmac.compare_digest(digest, solution.encode('ascii','ignore')):
conn = _engine.connect()
conn = config.engine.connect()
result = conn.execute(
select([_captcha]).where(_captcha.c.hmac == digest)
)
Expand Down
Loading

0 comments on commit 9de324d

Please sign in to comment.