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

Add helper classes for integration tests, refactor user managers and ankiserverctl.py #35

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
49 changes: 2 additions & 47 deletions AnkiServer/apps/sync_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
from anki.utils import intTime, checksum, isMac
from anki.consts import SYNC_ZIP_SIZE, SYNC_ZIP_COUNT

from AnkiServer.user_managers import SimpleUserManager, SqliteUserManager

try:
import simplejson as json
except ImportError:
Expand Down Expand Up @@ -338,25 +340,6 @@ def save(self, hkey, session):
def delete(self, hkey):
del self.sessions[hkey]

class SimpleUserManager(object):
"""A simple user manager that always allows any user."""

def authenticate(self, username, password):
"""
Returns True if this username is allowed to connect with this password. False otherwise.
Override this to change how users are authenticated.
"""

return True

def username2dirname(self, username):
"""
Returns the directory name for the given user. By default, this is just the username.
Override this to adjust the mapping between users and their directory.
"""

return username

class SyncApp(object):
valid_urls = SyncCollectionHandler.operations + SyncMediaHandler.operations + ['hostKey', 'upload', 'download', 'getDecks']

Expand Down Expand Up @@ -730,34 +713,6 @@ def delete(self, hkey):
cursor.execute("DELETE FROM session WHERE hkey=?", (hkey,))
conn.commit()

class SqliteUserManager(SimpleUserManager):
"""Authenticates users against a SQLite database."""

def __init__(self, auth_db_path):
self.auth_db_path = os.path.abspath(auth_db_path)

def authenticate(self, username, password):
"""Returns True if this username is allowed to connect with this password. False otherwise."""

conn = sqlite.connect(self.auth_db_path)
cursor = conn.cursor()
param = (username,)

cursor.execute("SELECT hash FROM auth WHERE user=?", param)

db_ret = cursor.fetchone()

if db_ret != None:
db_hash = str(db_ret[0])
salt = db_hash[-16:]
hashobj = hashlib.sha256()

hashobj.update(username+password+salt)

conn.close()

return (db_ret != None and hashobj.hexdigest()+salt == db_hash)

# Our entry point
def make_app(global_conf, **local_conf):
if local_conf.has_key('session_db_path'):
Expand Down
173 changes: 173 additions & 0 deletions AnkiServer/user_managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-


import binascii
import hashlib
import logging
import os
import sqlite3 as sqlite


class SimpleUserManager(object):
"""A simple user manager that always allows any user."""

def __init__(self, collection_path=''):
self.collection_path = collection_path

def authenticate(self, username, password):
"""
Returns True if this username is allowed to connect with this password.
False otherwise. Override this to change how users are authenticated.
"""

return True

def username2dirname(self, username):
"""
Returns the directory name for the given user. By default, this is just
the username. Override this to adjust the mapping between users and
their directory.
"""

return username

def _create_user_dir(self, username):
user_dir_path = os.path.join(self.collection_path, username)
if not os.path.isdir(user_dir_path):
logging.info("Creating collection directory for user '{}' at {}"
.format(username, user_dir_path))
os.makedirs(user_dir_path)


class SqliteUserManager(SimpleUserManager):
"""Authenticates users against a SQLite database."""

def __init__(self, auth_db_path, collection_path=None):
SimpleUserManager.__init__(self, collection_path)
self.auth_db_path = auth_db_path

def auth_db_exists(self):
return os.path.isfile(self.auth_db_path)

def user_list(self):
if not self.auth_db_exists():
raise ValueError("Cannot list users for nonexistent auth db {}."
.format(self.auth_db_path))
else:
conn = sqlite.connect(self.auth_db_path)
cursor = conn.cursor()
cursor.execute("SELECT user FROM auth")
rows = cursor.fetchall()
conn.commit()
conn.close()

return [row[0] for row in rows]

def user_exists(self, username):
users = self.user_list()
return username in users

def del_user(self, username):
if not self.auth_db_exists():
raise ValueError("Cannot remove user from nonexistent auth db {}."
.format(self.auth_db_path))
else:
conn = sqlite.connect(self.auth_db_path)
cursor = conn.cursor()
logging.info("Removing user '{}' from auth db."
.format(username))
cursor.execute("DELETE FROM auth WHERE user=?", (username,))
conn.commit()
conn.close()

def add_user(self, username, password):
self._add_user_to_auth_db(username, password)
self._create_user_dir(username)

def add_users(self, users_data):
for username, password in users_data:
self.add_user(username, password)

def _add_user_to_auth_db(self, username, password):
if not self.auth_db_exists():
self.create_auth_db()

pass_hash = self._create_pass_hash(username, password)

conn = sqlite.connect(self.auth_db_path)
cursor = conn.cursor()
logging.info("Adding user '{}' to auth db.".format(username))
cursor.execute("INSERT INTO auth VALUES (?, ?)",
(username, pass_hash))
conn.commit()
conn.close()

def set_password_for_user(self, username, new_password):
if not self.auth_db_exists():
raise ValueError("Cannot remove user from nonexistent auth db {}."
.format(self.auth_db_path))
elif not self.user_exists(username):
raise ValueError("Cannot remove nonexistent user {}."
.format(username))
else:
hash = self._create_pass_hash(username, new_password)

conn = sqlite.connect(self.auth_db_path)
cursor = conn.cursor()
cursor.execute("UPDATE auth SET hash=? WHERE user=?", (hash, username))
conn.commit()
conn.close()

logging.info("Changed password for user {}.".format(username))

def authenticate_user(self, username, password):
"""Returns True if this username is allowed to connect with this password. False otherwise."""

conn = sqlite.connect(self.auth_db_path)
cursor = conn.cursor()
param = (username,)
cursor.execute("SELECT hash FROM auth WHERE user=?", param)
db_hash = cursor.fetchone()
conn.close()

if db_hash is None:
logging.info("Authentication failed for nonexistent user {}."
.format(username))
return False
else:
expected_value = str(db_hash[0])
salt = self._extract_salt(expected_value)

hashobj = hashlib.sha256()
hashobj.update(username + password + salt)
actual_value = hashobj.hexdigest() + salt

if actual_value == expected_value:
logging.info("Authentication succeeded for user {}."
.format(username))
return True
else:
logging.info("Authentication failed for user {}."
.format(username))
return False

@staticmethod
def _extract_salt(hash):
return hash[-16:]

@staticmethod
def _create_pass_hash(username, password):
salt = binascii.b2a_hex(os.urandom(8))
pass_hash = (hashlib.sha256(username + password + salt).hexdigest() +
salt)
return pass_hash

def create_auth_db(self):
conn = sqlite.connect(self.auth_db_path)
cursor = conn.cursor()
logging.info("Creating auth db at {}."
.format(self.auth_db_path))
cursor.execute("""CREATE TABLE IF NOT EXISTS auth
(user VARCHAR PRIMARY KEY, hash VARCHAR)""")
conn.commit()
conn.close()
Loading