Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use salted hash for storing passwords #14

Merged
merged 2 commits into from
Nov 5, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions backend/src/logzen/db/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,14 @@ class Stream(Entity):
nullable=False,
autoincrement=True)

user_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('users.id'))
user_id = sqlalchemy.Column(sqlalchemy.ForeignKey('users.id'))

user = sqlalchemy.orm.relationship('User')

name = sqlalchemy.Column(sqlalchemy.String,
name = sqlalchemy.Column(sqlalchemy.Unicode,
nullable=False)

description = sqlalchemy.Column(sqlalchemy.Text,
description = sqlalchemy.Column(sqlalchemy.UnicodeText,
nullable=False,
default='')

Expand Down
42 changes: 11 additions & 31 deletions backend/src/logzen/db/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,11 @@
import sqlalchemy
import sqlalchemy.types
import sqlalchemy.orm.exc
from sqlalchemy.orm import validates

import hashlib


class Password(sqlalchemy.types.TypeDecorator):
""" A sqlalchemy type wrapper storing an password in a secured.

The password is hashed while setting the value. A 'decryption' is not
possible.

In addition to stored values, literals are hashed in the same way
allowing comparison of passwords.
"""

# TODO: Use passlib to store a salt

impl = sqlalchemy.String

def process_bind_param(self, value, dialect):
if value is not None:
return hashlib.sha512(value.encode('utf8')).hexdigest()
from sqlalchemy.orm import validates

from sqlalchemy_utils.types.password import PasswordType, Password

def process_literal_param(self, value, dialect):
if value is not None:
return hashlib.sha512(value.encode('utf8')).hexdigest()


class User(Entity):
Expand All @@ -64,11 +42,11 @@ class User(Entity):
nullable=False,
autoincrement=True)

username = sqlalchemy.Column(sqlalchemy.String,
username = sqlalchemy.Column(sqlalchemy.Unicode,
nullable=False,
unique=True)

password = sqlalchemy.Column(Password,
password = sqlalchemy.Column(PasswordType(schemes=['pbkdf2_sha512']),
nullable=False)

admin = sqlalchemy.Column(sqlalchemy.Boolean,
Expand Down Expand Up @@ -123,16 +101,18 @@ def getVerifiedUser(self, username, password):
If a user with such username exists and the users password matches
the given one the user entity is returned - None otherwise.
"""

try:
return self.session \
.query(User) \
.filter(User.username == username, \
User.password == password) \
.one()
user = self.getUser(username)

except sqlalchemy.orm.exc.NoResultFound:
return None

if user.password != password:
return None

return user


def getUsers(self):
return iter(self.session.query(User))
Expand Down
34 changes: 1 addition & 33 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,8 @@



@extend('logzen.config:ConfigFile')
def TestConfigFile(file):
from configparser import ConfigParser

config_file = ConfigParser()
config_file.add_section('db')
config_file.add_section('es')
config_file.add_section('auth')
config_file.add_section('log')

config_file.set('db', 'url', 'sqlite:///')
config_file.set('es', 'hosts', '')
config_file.set('log', 'level', 'DEBUG')

return config_file



@export(engine='logzen.db:Engine')
def TestConnection(engine):
return engine.connect()



@extend('logzen.db:SessionFactory',
connection='test_api:TestConnection')
def TestSessionFactory(factory,
connection):
factory.configure(bind=connection)



class ApiTestCase(unittest.TestCase):
testConnection = require('test_api:TestConnection')
testConnection = require('util:TestConnection')

app = require('logzen.web:App')

Expand Down
70 changes: 70 additions & 0 deletions backend/tests/test_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import unittest

from hamcrest import *

from require import *

from logzen.db import session
from logzen.db.users import User, Password



class DatabaseTestCase(unittest.TestCase):
testConnection = require('util:TestConnection')

session = require('logzen.db:Session')


def setUp(self):
# Begin a manual transaction
self.transaction = self.testConnection.begin()



def tearDown(self):
self.session.rollback()
self.session.close()

# Rollback all changes
self.transaction.rollback()



class UserDatabaseTestCase(DatabaseTestCase):

users = require('logzen.db.users:Users')


def setUp(self):
super(UserDatabaseTestCase, self).setUp()

with session():
self.users.createUser(username='test',
password='plain')

def testPasswordIsHashedAfterCreation(self):
user = User(username='test',
password='plain')

assert_that(user.password, is_(instance_of(Password)))


def testPasswordIsHashedAfterFetching(self):
user = self.users.getUser('test')

assert_that(user.password, is_(instance_of(Password)))


def testPasswordVerificytionSucceeds(self):
user = self.users.getUser('test')

assert_that(user.password == 'plain', is_(True))


def testPasswordVerificytionWithWrongPasswordFails(self):
user = self.users.getUser('test')

assert_that(user.password == 'WRONG!', is_(False))



35 changes: 35 additions & 0 deletions backend/tests/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@



from require import *


@extend('logzen.config:ConfigFile')
def TestConfigFile(file):
from configparser import ConfigParser

config_file = ConfigParser()
config_file.add_section('db')
config_file.add_section('es')
config_file.add_section('auth')
config_file.add_section('log')

config_file.set('db', 'url', 'sqlite:///')
config_file.set('es', 'hosts', '')
config_file.set('log', 'level', 'DEBUG')

return config_file



@export(engine='logzen.db:Engine')
def TestConnection(engine):
return engine.connect()



@extend('logzen.db:SessionFactory',
connection='util:TestConnection')
def TestSessionFactory(factory,
connection):
factory.configure(bind=connection)