Skip to content

Commit 6d5ab52

Browse files
authored
Release v2.1.0 (#595)
1 parent 33ac548 commit 6d5ab52

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1184
-225
lines changed

ckanext/querytool/authenticator.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import logging
2+
from repoze.who.interfaces import IAuthenticator
3+
from webob.request import Request
4+
from zope.interface import implements
5+
6+
from ckan.lib.authenticator import UsernamePasswordAuthenticator
7+
8+
from ckanext.querytool.model import VitalsSecurityTOTP
9+
10+
log = logging.getLogger(__name__)
11+
12+
13+
class VitalsTOTPAuth(UsernamePasswordAuthenticator):
14+
implements(IAuthenticator)
15+
16+
def authenticate(self, environ, identity):
17+
try:
18+
user_name = identity['login']
19+
except KeyError:
20+
return None
21+
22+
if not ('login' in identity and 'password' in identity):
23+
return None
24+
25+
# Run through the CKAN auth sequence first, so we can hit the DB
26+
# in every case and make timing attacks a little more difficult.
27+
auth_user_name = super(VitalsTOTPAuth, self).authenticate(
28+
environ, identity
29+
)
30+
31+
if auth_user_name is None:
32+
return None
33+
34+
return self.authenticate_totp(environ, auth_user_name)
35+
36+
def authenticate_totp(self, environ, auth_user):
37+
totp_challenger = VitalsSecurityTOTP.get_for_user(auth_user)
38+
39+
# if there is no totp configured, don't allow auth
40+
# shouldn't happen, login flow should create a totp_challenger
41+
if totp_challenger is None:
42+
log.info("Login attempted without MFA configured for: {}".format(
43+
auth_user)
44+
)
45+
return None
46+
47+
form_vars = environ.get('webob._parsed_post_vars')
48+
form_vars = dict(form_vars[0].items())
49+
50+
if not form_vars.get('mfa'):
51+
log.info("Could not get MFA credentials from the request")
52+
return None
53+
54+
result = totp_challenger.check_code(
55+
form_vars.get('mfa'),
56+
totp_challenger.created_at,
57+
form_vars.get('login')
58+
)
59+
60+
if result:
61+
return auth_user

ckanext/querytool/commands/totp.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from ckan.lib.cli import CkanCommand
2+
3+
import sys
4+
5+
6+
class VitalsSecurity(CkanCommand):
7+
'''Command for initializing the TOTP table
8+
Usage: paster --plugin=ckanext-querytool totp <command> -c <path to config file>
9+
10+
command:
11+
help - prints this help
12+
init_totp - create the database table to support time based one time (TOTP) login
13+
'''
14+
summary = __doc__.split('\n')[0]
15+
usage = __doc__
16+
17+
def command(self):
18+
# load pylons config
19+
self._load_config()
20+
options = {
21+
'init_totp': self.init_totp,
22+
'help': self.help,
23+
}
24+
25+
try:
26+
cmd = self.args[0]
27+
options[cmd](*self.args[1:])
28+
except KeyError:
29+
self.help()
30+
sys.exit(1)
31+
32+
def help(self):
33+
print(self.__doc__)
34+
35+
def init_totp(self):
36+
print(
37+
"Initializing database for multi-factor authentication "
38+
"(TOTP - Time-based One-Time Password)"
39+
)
40+
from ckanext.querytool.model import VitalsSecurityTOTP
41+
vs_totp = VitalsSecurityTOTP()
42+
vs_totp.totp_db_setup()
43+
print("Finished tables setup for multi-factor authentication")
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import logging
2+
import pyotp
3+
4+
import ckan.lib.base as base
5+
import ckan.model as model
6+
import ckan.lib.mailer as mailer
7+
import ckan.logic as logic
8+
from ckan.lib.mailer import mail_user
9+
from ckan.common import _, config
10+
import datetime
11+
12+
from ckanext.querytool.model import VitalsSecurityTOTP
13+
14+
log = logging.getLogger(__name__)
15+
16+
get_action = logic.get_action
17+
18+
19+
class QuerytoolEmailAuthController(base.BaseController):
20+
def send_2fa_code(self, user):
21+
try:
22+
user_dict = get_action('user_show')(None, {'id': user})
23+
# Get user object instead
24+
user_obj = model.User.get(user)
25+
vs_totp = VitalsSecurityTOTP()
26+
now = datetime.datetime.utcnow()
27+
challenge = vs_totp.create_for_user(
28+
user_dict['name'], created_at=now
29+
)
30+
secret = challenge.secret
31+
totp = pyotp.TOTP(secret)
32+
current_code = totp.at(for_time=now)
33+
user_display_name = user_dict['display_name']
34+
user_email = user_dict['email']
35+
email_subject = _('Verification Code')
36+
email_body = _(
37+
'Hi {user},\n\nHere\'s your verification'
38+
' code to login: {code}\n\nHave a great day!').format(
39+
user=user_display_name,
40+
code=current_code
41+
)
42+
site_title = config.get('ckan.site_title')
43+
site_url = config.get('ckan.site_url')
44+
45+
email_body += '\n\n' + site_title + '\n' + site_url
46+
47+
mailer.mail_recipient(
48+
user_display_name,
49+
user_email,
50+
email_subject,
51+
email_body
52+
)
53+
54+
return {'success': True, 'msg': 'Email sent'}
55+
except Exception as e:
56+
log.error(e)
57+
return {'success': False, 'msg': 'Error sending email'}

ckanext/querytool/controllers/querytool.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,8 @@ def line_attr_search(data, id, line_attr):
672672
data['map_title_field_{}'.format(id)]
673673
map_item['map_custom_title_field'] = \
674674
data.get('map_custom_title_field_{}'.format(id))
675+
map_item['map_infobox_title'] = \
676+
data.get('map_infobox_title_{}'.format(id))
675677
map_item['map_key_field'] = \
676678
data['map_key_field_{}'.format(id)]
677679
map_item['data_key_field'] = \

ckanext/querytool/controllers/user.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,31 @@
1717
get_action = logic.get_action
1818
render = base.render
1919
abort = base.abort
20+
MFA_ENABLED = asbool(config.get('ckanext.querytool.2fa_enabled', False))
21+
2022

2123
class QuerytoolUserController(UserController):
2224
def login(self, error=None):
2325
if request.method == 'POST':
2426
if check_recaptcha(request):
2527
base_url = config.get('ckan.site_url')
2628
came_from = request.params.get('came_from')
27-
29+
2830
# Full login URL
2931
url = base_url + h.url_for(
3032
self._get_repoze_handler('login_handler_path'),
3133
came_from=came_from)
3234

3335
username = request.params.get('login')
3436
password = request.params.get('password')
35-
37+
3638
# Login form data
3739
data = { 'login': username, 'password': password }
38-
40+
41+
if MFA_ENABLED:
42+
mfa = request.params.get('mfa')
43+
data['mfa'] = mfa
44+
3945
# Login request headers
4046
headers = { 'Content-Type': 'application/x-www-form-urlencoded' }
4147

@@ -56,11 +62,14 @@ def login(self, error=None):
5662
('Location', '/dashboard'),
5763
('Set-Cookie', set_cookie)
5864
]
59-
65+
6066
return response
6167
else:
62-
error = _('Login failed. Bad username or password.')
63-
68+
if MFA_ENABLED:
69+
error = _('Login failed. Bad username, password, or authentication code.')
70+
else:
71+
error = _('Login failed. Bad username or password.')
72+
6473
else:
6574
error = _('reCAPTCHA validation failed')
6675

@@ -76,7 +85,7 @@ def login(self, error=None):
7685
came_from = request.params.get('came_from')
7786
if not came_from:
7887
came_from = h.url_for(controller='user', action='logged_in')
79-
88+
8089
recaptcha_config = querytool_helpers.get_recaptcha_config()
8190
recaptcha_enabled = recaptcha_config.get('enabled', False)
8291

@@ -111,7 +120,10 @@ def logged_in(self):
111120

112121
return self.me()
113122
else:
114-
err = _('Login failed. Bad username or password.')
123+
if MFA_ENABLED:
124+
err = _('Login failed. Bad username, password, or authentication code.')
125+
else:
126+
err = _('Login failed. Bad username or password.')
115127
if asbool(config.get('ckan.legacy_templates', 'false')):
116128
h.flash_error(err)
117129
h.redirect_to(controller='user',

ckanext/querytool/fanstatic/css/main.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ckanext/querytool/fanstatic/css/public-query-tool.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ckanext/querytool/fanstatic/javascript/dist/modules/fullscreen.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ckanext/querytool/fanstatic/javascript/dist/modules/fullscreen.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ckanext/querytool/fanstatic/javascript/dist/modules/map-module.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)