Skip to content

Commit

Permalink
Release v2.1.0 (#595)
Browse files Browse the repository at this point in the history
  • Loading branch information
mpolidori authored Jun 19, 2023
1 parent 33ac548 commit 6d5ab52
Show file tree
Hide file tree
Showing 41 changed files with 1,184 additions and 225 deletions.
61 changes: 61 additions & 0 deletions ckanext/querytool/authenticator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import logging
from repoze.who.interfaces import IAuthenticator
from webob.request import Request
from zope.interface import implements

from ckan.lib.authenticator import UsernamePasswordAuthenticator

from ckanext.querytool.model import VitalsSecurityTOTP

log = logging.getLogger(__name__)


class VitalsTOTPAuth(UsernamePasswordAuthenticator):
implements(IAuthenticator)

def authenticate(self, environ, identity):
try:
user_name = identity['login']
except KeyError:
return None

if not ('login' in identity and 'password' in identity):
return None

# Run through the CKAN auth sequence first, so we can hit the DB
# in every case and make timing attacks a little more difficult.
auth_user_name = super(VitalsTOTPAuth, self).authenticate(
environ, identity
)

if auth_user_name is None:
return None

return self.authenticate_totp(environ, auth_user_name)

def authenticate_totp(self, environ, auth_user):
totp_challenger = VitalsSecurityTOTP.get_for_user(auth_user)

# if there is no totp configured, don't allow auth
# shouldn't happen, login flow should create a totp_challenger
if totp_challenger is None:
log.info("Login attempted without MFA configured for: {}".format(
auth_user)
)
return None

form_vars = environ.get('webob._parsed_post_vars')
form_vars = dict(form_vars[0].items())

if not form_vars.get('mfa'):
log.info("Could not get MFA credentials from the request")
return None

result = totp_challenger.check_code(
form_vars.get('mfa'),
totp_challenger.created_at,
form_vars.get('login')
)

if result:
return auth_user
43 changes: 43 additions & 0 deletions ckanext/querytool/commands/totp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from ckan.lib.cli import CkanCommand

import sys


class VitalsSecurity(CkanCommand):
'''Command for initializing the TOTP table
Usage: paster --plugin=ckanext-querytool totp <command> -c <path to config file>
command:
help - prints this help
init_totp - create the database table to support time based one time (TOTP) login
'''
summary = __doc__.split('\n')[0]
usage = __doc__

def command(self):
# load pylons config
self._load_config()
options = {
'init_totp': self.init_totp,
'help': self.help,
}

try:
cmd = self.args[0]
options[cmd](*self.args[1:])
except KeyError:
self.help()
sys.exit(1)

def help(self):
print(self.__doc__)

def init_totp(self):
print(
"Initializing database for multi-factor authentication "
"(TOTP - Time-based One-Time Password)"
)
from ckanext.querytool.model import VitalsSecurityTOTP
vs_totp = VitalsSecurityTOTP()
vs_totp.totp_db_setup()
print("Finished tables setup for multi-factor authentication")
57 changes: 57 additions & 0 deletions ckanext/querytool/controllers/email_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import logging
import pyotp

import ckan.lib.base as base
import ckan.model as model
import ckan.lib.mailer as mailer
import ckan.logic as logic
from ckan.lib.mailer import mail_user
from ckan.common import _, config
import datetime

from ckanext.querytool.model import VitalsSecurityTOTP

log = logging.getLogger(__name__)

get_action = logic.get_action


class QuerytoolEmailAuthController(base.BaseController):
def send_2fa_code(self, user):
try:
user_dict = get_action('user_show')(None, {'id': user})
# Get user object instead
user_obj = model.User.get(user)
vs_totp = VitalsSecurityTOTP()
now = datetime.datetime.utcnow()
challenge = vs_totp.create_for_user(
user_dict['name'], created_at=now
)
secret = challenge.secret
totp = pyotp.TOTP(secret)
current_code = totp.at(for_time=now)
user_display_name = user_dict['display_name']
user_email = user_dict['email']
email_subject = _('Verification Code')
email_body = _(
'Hi {user},\n\nHere\'s your verification'
' code to login: {code}\n\nHave a great day!').format(
user=user_display_name,
code=current_code
)
site_title = config.get('ckan.site_title')
site_url = config.get('ckan.site_url')

email_body += '\n\n' + site_title + '\n' + site_url

mailer.mail_recipient(
user_display_name,
user_email,
email_subject,
email_body
)

return {'success': True, 'msg': 'Email sent'}
except Exception as e:
log.error(e)
return {'success': False, 'msg': 'Error sending email'}
2 changes: 2 additions & 0 deletions ckanext/querytool/controllers/querytool.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,8 @@ def line_attr_search(data, id, line_attr):
data['map_title_field_{}'.format(id)]
map_item['map_custom_title_field'] = \
data.get('map_custom_title_field_{}'.format(id))
map_item['map_infobox_title'] = \
data.get('map_infobox_title_{}'.format(id))
map_item['map_key_field'] = \
data['map_key_field_{}'.format(id)]
map_item['data_key_field'] = \
Expand Down
28 changes: 20 additions & 8 deletions ckanext/querytool/controllers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,31 @@
get_action = logic.get_action
render = base.render
abort = base.abort
MFA_ENABLED = asbool(config.get('ckanext.querytool.2fa_enabled', False))


class QuerytoolUserController(UserController):
def login(self, error=None):
if request.method == 'POST':
if check_recaptcha(request):
base_url = config.get('ckan.site_url')
came_from = request.params.get('came_from')

# Full login URL
url = base_url + h.url_for(
self._get_repoze_handler('login_handler_path'),
came_from=came_from)

username = request.params.get('login')
password = request.params.get('password')

# Login form data
data = { 'login': username, 'password': password }


if MFA_ENABLED:
mfa = request.params.get('mfa')
data['mfa'] = mfa

# Login request headers
headers = { 'Content-Type': 'application/x-www-form-urlencoded' }

Expand All @@ -56,11 +62,14 @@ def login(self, error=None):
('Location', '/dashboard'),
('Set-Cookie', set_cookie)
]

return response
else:
error = _('Login failed. Bad username or password.')

if MFA_ENABLED:
error = _('Login failed. Bad username, password, or authentication code.')
else:
error = _('Login failed. Bad username or password.')

else:
error = _('reCAPTCHA validation failed')

Expand All @@ -76,7 +85,7 @@ def login(self, error=None):
came_from = request.params.get('came_from')
if not came_from:
came_from = h.url_for(controller='user', action='logged_in')

recaptcha_config = querytool_helpers.get_recaptcha_config()
recaptcha_enabled = recaptcha_config.get('enabled', False)

Expand Down Expand Up @@ -111,7 +120,10 @@ def logged_in(self):

return self.me()
else:
err = _('Login failed. Bad username or password.')
if MFA_ENABLED:
err = _('Login failed. Bad username, password, or authentication code.')
else:
err = _('Login failed. Bad username or password.')
if asbool(config.get('ckan.legacy_templates', 'false')):
h.flash_error(err)
h.redirect_to(controller='user',
Expand Down
2 changes: 1 addition & 1 deletion ckanext/querytool/fanstatic/css/main.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ckanext/querytool/fanstatic/css/public-query-tool.css

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Loading

0 comments on commit 6d5ab52

Please sign in to comment.