You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2016/09/07 17:56:45 UTC
[3/3] allura git commit: [#8118] refactor and share Google
Authenticator file handling, implement recovery codes for it
[#8118] refactor and share Google Authenticator file handling, implement recovery codes for it
Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/533d61e4
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/533d61e4
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/533d61e4
Branch: refs/heads/db/8118
Commit: 533d61e45bb1db6da83ea3f318cfcf74e57b0f1a
Parents: c044a01
Author: Dave Brondsema <da...@brondsema.net>
Authored: Tue Sep 6 15:23:47 2016 -0400
Committer: Dave Brondsema <da...@brondsema.net>
Committed: Wed Sep 7 13:54:24 2016 -0400
----------------------------------------------------------------------
Allura/allura/lib/multifactor.py | 85 ++++++++++++++++--------
Allura/allura/tests/test_multifactor.py | 97 ++++++++++++++++++++++------
2 files changed, 135 insertions(+), 47 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/allura/blob/533d61e4/Allura/allura/lib/multifactor.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/multifactor.py b/Allura/allura/lib/multifactor.py
index 95683df..cfd6247 100644
--- a/Allura/allura/lib/multifactor.py
+++ b/Allura/allura/lib/multifactor.py
@@ -193,11 +193,7 @@ class GoogleAuthenticatorFile(object):
return '\n'.join(lines)
-class GoogleAuthenticatorPamFilesystemTotpService(TotpService):
- '''
- Store in home directories, compatible with the TOTP PAM module for Google Authenticator
- https://github.com/google/google-authenticator/tree/master/libpam
- '''
+class GoogleAuthenticatorPamFilesystemMixin(object):
@property
def basedir(self):
@@ -209,39 +205,54 @@ class GoogleAuthenticatorPamFilesystemTotpService(TotpService):
raise ValueError('Insecure username contains "/": %s' % username)
return os.path.join(self.basedir, username, '.google_authenticator')
- def get_secret_key(self, user):
+ def read_file(self, user, autocreate=False):
+ if autocreate:
+ userdir = os.path.dirname(self.config_file(user))
+ if not os.path.exists(userdir):
+ os.makedirs(userdir, 0700)
+
try:
with open(self.config_file(user)) as f:
- gaf = GoogleAuthenticatorFile.load(f.read())
- return gaf.key
+ return GoogleAuthenticatorFile.load(f.read())
except IOError as e:
if e.errno == errno.ENOENT: # file doesn't exist
- return None
+ if autocreate:
+ gaf = GoogleAuthenticatorFile()
+ gaf.options['RATE_LIMIT'] = '3 30'
+ gaf.options['DISALLOW_REUSE'] = None
+ gaf.options['TOTP_AUTH'] = None
+ return gaf
+ else:
+ return None
else:
raise
+ def write_file(self, user, gaf):
+ with open(self.config_file(user), 'w') as f:
+ f.write(gaf.dump())
+
+
+class GoogleAuthenticatorPamFilesystemTotpService(TotpService, GoogleAuthenticatorPamFilesystemMixin):
+ '''
+ Store in home directories, compatible with the TOTP PAM module for Google Authenticator
+ https://github.com/google/google-authenticator/tree/master/libpam
+ '''
+
+ def get_secret_key(self, user):
+ gaf = self.read_file(user)
+ if gaf:
+ return gaf.key
+ else:
+ return None
+
def set_secret_key(self, user, key):
if key is None:
# this also deletes the recovery keys, since they're stored in the same file
os.remove(self.config_file(user))
else:
- userdir = os.path.dirname(self.config_file(user))
- if not os.path.exists(userdir):
- os.makedirs(userdir, 0700)
- try:
- with open(self.config_file(user)) as f:
- gaf = GoogleAuthenticatorFile.load(f.read())
- except IOError as e:
- if e.errno == errno.ENOENT: # file doesn't exist
- gaf = GoogleAuthenticatorFile()
- gaf.options['RATE_LIMIT'] = '3 30'
- gaf.options['DISALLOW_REUSE'] = None
- gaf.options['TOTP_AUTH'] = None
- else:
- raise
+ gaf = self.read_file(user, autocreate=True)
gaf.key = key
- with open(self.config_file(user), 'w') as f:
- f.write(gaf.dump())
+ self.write_file(user, gaf)
class RecoveryCodeService(object):
@@ -333,7 +344,27 @@ class MongodbRecoveryCodeService(RecoveryCodeService):
raise InvalidRecoveryCode
-class GoogleAuthenticatorPamFilesystemRecoveryCodeService(RecoveryCodeService):
+class GoogleAuthenticatorPamFilesystemRecoveryCodeService(RecoveryCodeService, GoogleAuthenticatorPamFilesystemMixin):
def get_codes(self, user):
- return []
+ gaf = self.read_file(user)
+ if gaf:
+ return gaf.recovery_codes
+ else:
+ return []
+
+ def replace_codes(self, user, codes):
+ gaf = self.read_file(user)
+ if gaf:
+ gaf.recovery_codes = codes
+ self.write_file(user, gaf)
+ elif codes:
+ raise IOError('No .google-authenticator file exists, cannot add recovery codes.')
+
+ def verify_and_remove_code(self, user, code):
+ gaf = self.read_file(user)
+ if gaf and code in gaf.recovery_codes:
+ gaf.recovery_codes.remove(code)
+ self.write_file(user, gaf)
+ return True
+ raise InvalidRecoveryCode
http://git-wip-us.apache.org/repos/asf/allura/blob/533d61e4/Allura/allura/tests/test_multifactor.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_multifactor.py b/Allura/allura/tests/test_multifactor.py
index 6828757..3c7bb03 100644
--- a/Allura/allura/tests/test_multifactor.py
+++ b/Allura/allura/tests/test_multifactor.py
@@ -14,8 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-
-
+import shutil
import textwrap
import os
@@ -32,6 +31,7 @@ from tg import config
from allura.lib.multifactor import GoogleAuthenticatorFile, TotpService, MongodbTotpService
from allura.lib.multifactor import GoogleAuthenticatorPamFilesystemTotpService
from allura.lib.multifactor import RecoveryCodeService, MongodbRecoveryCodeService
+from allura.lib.multifactor import GoogleAuthenticatorPamFilesystemRecoveryCodeService
class TestGoogleAuthenticatorFile(object):
@@ -154,11 +154,20 @@ class TestMongodbTotpService():
assert_equal(None, srv.get_secret_key(user))
-class TestGoogleAuthenticatorPamFilesystemTotpService():
- sample_key = b'\x00K\xda\xbfv\xc2B\xaa\x1a\xbe\xa5\x96b\xb2\xa0Z:\xc9\xcf\x8a'
+class TestGoogleAuthenticatorPamFilesystemMixin(object):
def setUp(self):
- config['auth.multifactor.totp.filesystem.basedir'] = os.path.join(os.getenv('TMPDIR', '/tmp'), 'totp-test')
+ self.totp_basedir = os.path.join(os.getenv('TMPDIR', '/tmp'), 'totp-test')
+ config['auth.multifactor.totp.filesystem.basedir'] = self.totp_basedir
+
+ def tearDown(self):
+ if os.path.exists(self.totp_basedir):
+ shutil.rmtree(self.totp_basedir)
+
+
+class TestGoogleAuthenticatorPamFilesystemTotpService(TestGoogleAuthenticatorPamFilesystemMixin):
+
+ sample_key = b'\x00K\xda\xbfv\xc2B\xaa\x1a\xbe\xa5\x96b\xb2\xa0Z:\xc9\xcf\x8a'
def test_none(self):
srv = GoogleAuthenticatorPamFilesystemTotpService()
@@ -202,24 +211,24 @@ class TestRecoveryCodeService(object):
assert_equal(len(recovery.saved_codes), asint(config.get('auth.multifactor.recovery_code.count', 10)))
-class TestMongodbRecoveryCodeService(object):
+class TestAnyRecoveryCodeServiceImplementation(object):
- def setUp(self):
- config = {
- 'ming.main.uri': 'mim://allura_test',
- }
- ming.configure(**config)
+ __test__ = False
- def test_get_codes(self):
- recovery = MongodbRecoveryCodeService()
- user = Mock(_id=bson.ObjectId())
+ def test_get_codes_none(self):
+ recovery = self.Service()
+ user = self.mock_user()
assert_equal(recovery.get_codes(user), [])
+
+ def test_regen_get_codes(self):
+ recovery = self.Service()
+ user = self.mock_user()
recovery.regenerate_codes(user)
assert recovery.get_codes(user)
def test_replace_codes(self):
- recovery = MongodbRecoveryCodeService()
- user = Mock(_id=bson.ObjectId())
+ recovery = self.Service()
+ user = self.mock_user()
codes = [
'12345',
'67890'
@@ -228,16 +237,16 @@ class TestMongodbRecoveryCodeService(object):
assert_equal(recovery.get_codes(user), codes)
def test_verify_fail(self):
- recovery = MongodbRecoveryCodeService()
- user = Mock(_id=bson.ObjectId())
+ recovery = self.Service()
+ user = self.mock_user()
with assert_raises(InvalidRecoveryCode):
recovery.verify_and_remove_code(user, '11111')
with assert_raises(InvalidRecoveryCode):
recovery.verify_and_remove_code(user, '')
def test_verify_and_remove_code(self):
- recovery = MongodbRecoveryCodeService()
- user = Mock(_id=bson.ObjectId())
+ recovery = self.Service()
+ user = self.mock_user()
codes = [
'12345',
'67890'
@@ -246,3 +255,51 @@ class TestMongodbRecoveryCodeService(object):
result = recovery.verify_and_remove_code(user, '12345')
assert_equal(result, True)
assert_equal(recovery.get_codes(user), ['67890'])
+
+
+class TestMongodbRecoveryCodeService(TestAnyRecoveryCodeServiceImplementation):
+
+ __test__ = True
+
+ Service = MongodbRecoveryCodeService
+
+ def setUp(self):
+ config = {
+ 'ming.main.uri': 'mim://allura_test',
+ }
+ ming.configure(**config)
+
+ def mock_user(self):
+ return Mock(_id=bson.ObjectId())
+
+
+class TestGoogleAuthenticatorPamFilesystemRecoveryCodeService(TestAnyRecoveryCodeServiceImplementation,
+ TestGoogleAuthenticatorPamFilesystemMixin):
+
+ __test__ = True
+
+ Service = GoogleAuthenticatorPamFilesystemRecoveryCodeService
+
+ def mock_user(self):
+ return Mock(username='some-user-guy')
+
+ def setUp(self):
+ super(TestGoogleAuthenticatorPamFilesystemRecoveryCodeService, self).setUp()
+
+ # make a regular .google-authenticator file first, so recovery keys have somewhere to go
+ GoogleAuthenticatorPamFilesystemTotpService().set_secret_key(self.mock_user(),
+ b'\x00K\xda\xbfv\xc2B\xaa\x1a\xbe\xa5\x96b\xb2\xa0Z:\xc9\xcf\x8a')
+
+ def test_get_codes_none_when_no_file(self):
+ # this deletes the file
+ GoogleAuthenticatorPamFilesystemTotpService().set_secret_key(self.mock_user(), None)
+
+ super(TestGoogleAuthenticatorPamFilesystemRecoveryCodeService, self).test_get_codes_none()
+
+ def test_replace_codes_when_no_file(self):
+ # this deletes the file
+ GoogleAuthenticatorPamFilesystemTotpService().set_secret_key(self.mock_user(), None)
+
+ # then it errors because no .google-authenticator file
+ with assert_raises(IOError):
+ super(TestGoogleAuthenticatorPamFilesystemRecoveryCodeService, self).test_replace_codes()