Merge pull request #74 from jokajak/flask_cleanup

Flask cleanup
This commit is contained in:
Giulio Cesare Solaroli 2015-08-03 09:56:48 +02:00
commit 721beef8c8
8 changed files with 273 additions and 56 deletions

6
backend/flask/src/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
bin/
lib/
include/
clipperz.egg-info/
.Python
app.db

View File

@ -1,3 +1,4 @@
"""Clipperz API handler."""
import json
import random
import hashlib
@ -12,27 +13,36 @@ from .exceptions import InvalidUsage
from .models import User, Record, RecordVersion, OneTimePassword
#==============================================================================
# ==============================================================================
# Helpers
#==============================================================================
# ==============================================================================
def randomSeed():
"""Generate a random seed."""
return hex(random.getrandbits(32*8))[2:-1]
def clipperzHash(aString):
"""Calculate a clipperz hash.
sha256(sha256(aString))
"""
firstRound = hashlib.sha256()
firstRound.update(aString)
result = hashlib.sha256()
result.update(firstRound.digest())
return result.hexdigest()
#==============================================================================
# ==============================================================================
# Method handlers
#==============================================================================
# ==============================================================================
class HandlerMixin:
"""Mixin for handling requests."""
def handle_request(self, request):
"""Default method to handle a request."""
parameters = json.loads(request.form['parameters'])
app.logger.debug('raw parameters: %s', parameters)
parameters = parameters['parameters']
@ -50,7 +60,14 @@ class HandlerMixin:
class registration(HandlerMixin):
"""Registration handler."""
def completeRegistration(self, parameters, request):
"""Complete a registration.
Create a new user.
"""
credentials = parameters['credentials']
data = parameters['user']
user = User()
@ -63,11 +80,21 @@ class registration(HandlerMixin):
class handshake(HandlerMixin):
"""Handshake handler.
This handles the logon process.
"""
srp_n = '115b8b692e0e045692cf280b436735c77a5a9e8a9e7ed56c965f87db5b2a2ece3'
srp_g = 2
srp_n = long(srp_n, 16)
def connect(self, parameters, request):
"""Process a connect request.
Attempt to log in by processing the parameters.
"""
result = {}
session['C'] = parameters['parameters']['C']
session['A'] = parameters['parameters']['A']
@ -127,8 +154,12 @@ class handshake(HandlerMixin):
return jsonify({'result': result})
def credentialCheck(self, parameters, request):
"""Check credentials.
Handles the SRP process.
"""
country = 'US'
# hard-coded for development
# hard-coded for development/personal use.
result = {
'accountInfo': {
'features': [
@ -203,14 +234,15 @@ class handshake(HandlerMixin):
return jsonify({'result': result})
def oneTimePassword(self, parameters, request):
#"parameters": {
#"message": "oneTimePassword",
#"version": "0.2",
#"parameters": {
# "oneTimePasswordKey": "03bd882...396082c",
# "oneTimePasswordKeyChecksum": "f73f629...041031d"
#}
#}
"""Handle one time password logins."""
# "parameters": {
# "message": "oneTimePassword",
# "version": "0.2",
# "parameters": {
# "oneTimePasswordKey": "03bd882...396082c",
# "oneTimePasswordKeyChecksum": "f73f629...041031d"
# }
# }
result = {}
try:
@ -237,8 +269,12 @@ class handshake(HandlerMixin):
class message(HandlerMixin):
"""Handle messages once logged in."""
@login_required
def getUserDetails(self, parameters, request):
"""Get a user's details."""
app.logger.debug(parameters)
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
@ -252,7 +288,7 @@ class message(HandlerMixin):
# Online results
# {"result":
# {
# "header": "{\"records\":{\"index\":{\"383036...eeefbe48\":\"0\"},\"data\":\"zrhb3/791SDdb48v3vXfPzeDrv0Jhs4rAaOKHx+jDF6pwm/qi9DGSR0JwrprOgwv3bjYJgU2xHA8cuA0bPvABHSHK6fnGwvhSlyYjskY2Cy/WbRJhcA4kw+VUsOjZPRxtM8bSJnSxViAXsghTcya6+5M3MdMJHE=\"},\"directLogins\":{\"index\":{},\"data\":\"s7KYzHwKISmjYufv9h0mpTiM\"},\"preferences\":{\"data\":\"mf8fWjpOQjlV18ukEO9FN3LP\"},\"oneTimePasswords\":{\"data\":\"8tV1yRHv30lsl3FadG9YnTOo\"},\"version\":\"0.1\"}",
# "header": "{\"records\":{\"index\":{\"383036...eeefbe48\":\"0\"},\"data\":\"zrhb3/791SDdb48v3vXfPzeDrv0Jhs4rAaOKHx+jDF6pwm/qi9DGSR0JwrprOgwv3bjYJgU2xHA8cuA0bPvABHSHK6fnGwvhSlyYjskY2Cy/WbRJhcA4kw+VUsOjZPRxtM8bSJnSxViAXsghTcya6+5M3MdMJHE=\"},\"directLogins\":{\"index\":{},\"data\":\"s7KYzHwKISmjYufv9h0mpTiM\"},\"preferences\":{\"data\":\"mf8fWjpOQjlV18ukEO9FN3LP\"},\"oneTimePasswords\":{\"data\":\"8tV1yRHv30lsl3FadG9YnTOo\"},\"version\":\"0.1\"}", # NOQA
# "lock": "3D4B4501-D7A9-6E4F-A487-9428C0B6E79D",
# "version": "0.4",
# "recordsStats": {
@ -266,8 +302,8 @@ class message(HandlerMixin):
# }
# }
# Dev results
#{"result":
# {"header": "{\"records\":{\"index\":{\"843a95d8...5f734b\":\"1\"},\"data\":\"fKgc5Jt9JH/CibCIpcRmwyLuLIvufWchNJga7GoFcWT9K8LR+ai0BvzWBUxcPccivE9zPv2Swe5E8wPEIc+Lv0U73NobJEct7WqBcCdLxszBE1SokxPEZDUVdWVQtAiwgOS219inCFmI5CaB\"},\"directLogins\":{\"index\":{},\"data\":\"rnMQBB81ezh6JKNGXkDCyY+q\"},\"preferences\":{\"data\":\"9jzR9Goo5PGpXbAdmsXHuQGp\"},\"oneTimePasswords\":{\"data\":\"iXEUuQGskZhMyHEwU+3tRGQM\"},\"version\":\"0.1\"}",
# {"result":
# {"header": "{\"records\":{\"index\":{\"843a95d8...5f734b\":\"1\"},\"data\":\"fKgc5Jt9JH/CibCIpcRmwyLuLIvufWchNJga7GoFcWT9K8LR+ai0BvzWBUxcPccivE9zPv2Swe5E8wPEIc+Lv0U73NobJEct7WqBcCdLxszBE1SokxPEZDUVdWVQtAiwgOS219inCFmI5CaB\"},\"directLogins\":{\"index\":{},\"data\":\"rnMQBB81ezh6JKNGXkDCyY+q\"},\"preferences\":{\"data\":\"9jzR9Goo5PGpXbAdmsXHuQGp\"},\"oneTimePasswords\":{\"data\":\"iXEUuQGskZhMyHEwU+3tRGQM\"},\"version\":\"0.1\"}", # NOQA
# "recordStats": {
# "843a95d8...5f734b": {
# "updateDate": "Sun, 12 Apr 2015 13:08:44 GMT"
@ -295,6 +331,7 @@ class message(HandlerMixin):
@login_required
def saveChanges(self, parameters, request):
"""Save changes to a user's settings."""
result = {}
parameters = parameters['parameters']
if ('user' not in parameters
@ -340,7 +377,8 @@ class message(HandlerMixin):
@login_required
def getRecordDetail(self, parameters, request):
#{
"""Get details about a record."""
# {
# "parameters": {
# "srpSharedSecret": "bf79ad3cf0c1...63462a9fb560",
# "message": "getRecordDetail",
@ -348,7 +386,7 @@ class message(HandlerMixin):
# "reference": "e3a5856...20e080fc97f13c14c"
# }
# }
#}
# }
app.logger.debug(parameters)
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
@ -404,13 +442,14 @@ class message(HandlerMixin):
@login_required
def getOneTimePasswordsDetails(self, parameters, request):
#{
"""Get details about a one time password."""
# {
# "parameters": {
# "srpSharedSecret": "bf79ad3cf0c1...63462a9fb560",
# "message": "getOneTimePasswordsDetails",
# "parameters": {}
# }
#}
# }
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
'Mal-formed message format.',
@ -425,20 +464,24 @@ class message(HandlerMixin):
otps = OneTimePassword().query.filter_by(user=current_user).all()
for otp in otps:
#{"e8541...af0c6b951":{"status":"ACTIVE"}}
# {"e8541...af0c6b951":{"status":"ACTIVE"}}
result[otp.reference] = {'status': otp.status}
return jsonify({'result': result})
@login_required
def getLoginHistory(self, parameters, request):
#{
"""Get login history.
Not currently fully implemented.
"""
# {
# "parameters": {
# "srpSharedSecret": "bf79ad3cf0c1...63462a9fb560",
# "message": "getOneTimePasswordsDetails",
# "parameters": {}
# }
#}
# }
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
'Mal-formed message format.',
@ -462,6 +505,7 @@ class message(HandlerMixin):
@login_required
def addNewOneTimePassword(self, parameters, request):
"""Add a new one time password."""
# "parameters": {
# "message": "addNewOneTimePassword",
# "srpSharedSecret": "1e8e037a8...85680f931d45dfc20472cf9d1",
@ -481,7 +525,7 @@ class message(HandlerMixin):
# }
# }
# }
#}
# }
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
'Mal-formed message format.',
@ -524,6 +568,7 @@ class message(HandlerMixin):
return jsonify({'result': result})
def echo(self, parameters, request):
"""Check the status of the session."""
result = {}
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
@ -549,6 +594,7 @@ class message(HandlerMixin):
@login_required
def deleteUser(self, parameters, request):
"""Delete a user and all of its records."""
result = {}
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
@ -573,7 +619,8 @@ class message(HandlerMixin):
@login_required
def upgradeUserCredentials(self, parameters, request):
#{"parameters":{"message":"upgradeUserCredentials","srpSharedSecret":"36...d6","parameters":{"credentials":{"C":"59d02038fdb47cee5b7837a697bc8ff41cc66d8844c8fce844cdf45b0b08b1e4","s":"fe40513b99fbaca9bfe51b8d6e9b3eb42b1e01ce8b0ae32461bec0294c1030ed","v":"300b92f4a3e34034d78cd5081f8db36dbf2a4c5f7a41db6954518815a3554278","version":"0.2"},"user":{"header":"{\"records\":{\"index\":{},\"data\":\"VIIDc5vFNoIflyXF8syb8fRS\"},\"directLogins\":{\"index\":{},\"data\":\"9elg3tu2UqsJ0zbUAdQkLE69\"},\"preferences\":{\"data\":\"Sbwar35Ynd/XobuAm4K66lqj\"},\"oneTimePasswords\":{\"data\":\"tAcTsWVTwALSfxXvCChHi4FD\"},\"version\":\"0.1\"}","statistics":"","version":"0.4","lock":null}}}}
"""Upgrade a user's credentials to a new password."""
# {"parameters":{"message":"upgradeUserCredentials","srpSharedSecret":"36...d6","parameters":{"credentials":{"C":"59d02038fdb47cee5b7837a697bc8ff41cc66d8844c8fce844cdf45b0b08b1e4","s":"fe40513b99fbaca9bfe51b8d6e9b3eb42b1e01ce8b0ae32461bec0294c1030ed","v":"300b92f4a3e34034d78cd5081f8db36dbf2a4c5f7a41db6954518815a3554278","version":"0.2"},"user":{"header":"{\"records\":{\"index\":{},\"data\":\"VIIDc5vFNoIflyXF8syb8fRS\"},\"directLogins\":{\"index\":{},\"data\":\"9elg3tu2UqsJ0zbUAdQkLE69\"},\"preferences\":{\"data\":\"Sbwar35Ynd/XobuAm4K66lqj\"},\"oneTimePasswords\":{\"data\":\"tAcTsWVTwALSfxXvCChHi4FD\"},\"version\":\"0.1\"}","statistics":"","version":"0.4","lock":null}}}} # NOQA
result = {}
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
@ -614,7 +661,11 @@ class message(HandlerMixin):
class logout(HandlerMixin):
"""Logout handler."""
def handle_request(self, request):
"""Handle a logout request."""
result = {}
logout_user()
session.clear()

View File

@ -1,3 +1,4 @@
"""Clipperz models."""
import datetime
from flask.ext.login import UserMixin
@ -6,15 +7,18 @@ from clipperz import app, db
class User(db.Model, UserMixin):
"""Clipperz User model."""
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(), unique=True, index=True)
username = db.Column(db.String(255), unique=True, index=True)
srp_s = db.Column(db.String(128))
srp_v = db.Column(db.String(128))
header = db.Column(db.Text())
statistics = db.Column(db.Text())
auth_version = db.Column(db.String())
version = db.Column(db.String())
lock = db.Column(db.String())
auth_version = db.Column(db.String(255))
version = db.Column(db.String(255))
lock = db.Column(db.String(255))
records = db.relationship(
'Record',
backref='user',
@ -29,12 +33,14 @@ class User(db.Model, UserMixin):
update_date = db.Column(db.DateTime(), nullable=True)
def updateCredentials(self, credentials):
"""Update user credentials."""
self.username = credentials['C']
self.srp_s = credentials['s']
self.srp_v = credentials['v']
self.auth_version = credentials['version']
def update(self, data):
"""Update user object."""
self.header = data['header']
self.statistics = data['statistics']
self.version = data['version']
@ -44,24 +50,33 @@ class User(db.Model, UserMixin):
self.offline_saved = False
def __repr__(self):
"""User representation."""
return '<User %r>' % (self.username)
#------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class RecordVersion(db.Model):
"""
Model a RecordVersion.
RecordVersion store attributes associated with a specific version of a
record.
"""
id = db.Column(db.Integer(), primary_key=True)
reference = db.Column(db.String(), unique=True, index=True)
reference = db.Column(db.String(255), unique=True, index=True)
header = db.Column(db.Text())
data = db.Column(db.Text())
api_version = db.Column(db.String())
api_version = db.Column(db.String(255))
version = db.Column(db.Integer())
previous_version_key = db.Column(db.String())
previous_version_key = db.Column(db.String(255))
previous_version_id = db.Column(db.Integer(),
db.ForeignKey('record_version.id'))
creation_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
update_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
access_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
creation_date = db.Column(db.DateTime())
update_date = db.Column(db.DateTime())
access_date = db.Column(db.DateTime())
record_id = db.Column(db.Integer(),
db.ForeignKey('record.id'),
@ -72,7 +87,12 @@ class RecordVersion(db.Model):
order_by=id,
cascade='all,delete'))
def __init__(self):
"""Initialize a record version."""
self.creation_date = datetime.datetime.utcnow()
def update(self, someData):
"""Update a record version."""
app.logger.debug(someData)
recordVersionData = someData['currentRecordVersion']
self.reference = recordVersionData['reference']
@ -83,26 +103,37 @@ class RecordVersion(db.Model):
self.update_date = datetime.datetime.utcnow()
self.record.update(someData['record'], self)
#------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class Record(db.Model):
"""Model a record.
A Record has multiple record versions.
"""
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.ForeignKey('user.id'))
reference = db.Column(db.String(), unique=True, index=True)
reference = db.Column(db.String(255), unique=True, index=True)
data = db.Column(db.Text())
api_version = db.Column(db.String())
api_version = db.Column(db.String(255))
version = db.Column(db.Integer(), default=0)
creation_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
update_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
access_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
creation_date = db.Column(db.DateTime())
update_date = db.Column(db.DateTime())
access_date = db.Column(db.DateTime())
current_record_version = db.relationship(
'RecordVersion',
uselist=False,
cascade='save-update, merge, delete, delete-orphan')
def __init__(self):
"""Initialize a record."""
self.creation_date = datetime.datetime.utcnow()
def update(self, data, record_version):
"""Update a record."""
self.reference = data['reference']
self.data = data['data']
self.api_version = data['version']
@ -113,23 +144,34 @@ class Record(db.Model):
else:
self.version = 1
#------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class OneTimePassword(db.Model):
"""Model a OneTimePassword.
OneTimePasswords are used to log in to clipperz only once.
"""
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.ForeignKey('user.id'))
status = db.Column(db.String())
reference = db.Column(db.String(), unique=True)
key_value = db.Column(db.String())
key_checksum = db.Column(db.String())
status = db.Column(db.String(255))
reference = db.Column(db.String(255), unique=True)
key_value = db.Column(db.String(255))
key_checksum = db.Column(db.String(255))
data = db.Column(db.Text())
version = db.Column(db.String())
creation_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
version = db.Column(db.String(255))
creation_date = db.Column(db.DateTime())
request_date = db.Column(db.DateTime())
usage_date = db.Column(db.DateTime())
def __init__(self):
"""Initialize a OneTimePassword."""
self.creation_date = datetime.datetime.utcnow()
def update(self, someParameters, aStatus):
"""Update a one time password."""
self.reference = someParameters['reference']
self.key_value = someParameters['key']
self.key_checksum = someParameters['keyChecksum']
@ -138,14 +180,22 @@ class OneTimePassword(db.Model):
self.status = aStatus
def reset(self, aStatus):
"""Reset a one time password."""
self.data = ""
self.status = aStatus
return self
#------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class Session(db.Model):
"""Model a session."""
id = db.Column(db.Integer(), primary_key=True)
sessionId = db.Column(db.String())
access_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
sessionId = db.Column(db.String(255))
access_date = db.Column(db.DateTime())
def __init__(self):
"""Initialize a session."""
self.access_date = datetime.datetime.utcnow()

View File

@ -1,23 +1,30 @@
from flask import session, request, g, jsonify
"""Clipperz views."""
from flask import session, request, g
from clipperz import app, db, lm
from .models import User
from .api import *
from .api import * # NOQA
from .exceptions import InvalidUsage
from flask.ext.login import login_required
@lm.user_loader
def load_user(id):
"""Load a user.
Converts a user id in to a User object.
"""
return User.query.get(int(id))
@app.before_request
def before_request():
"""Store the current user."""
g.user = current_user
@app.teardown_appcontext
def shutdown_session(exception=None):
"""Remove the session from the database."""
db.session.remove()
@ -26,6 +33,7 @@ def shutdown_session(exception=None):
@app.route('/delta/dump/<string:frontend_version>')
@login_required
def dump(frontend_version):
"""Return JSON for a user's data."""
user = User().query.filter_by(username=session['C']).one()
if (user != g.user):
@ -104,21 +112,25 @@ def dump(frontend_version):
@app.route('/beta/<path:path>')
def beta(path):
"""Fallback for serving beta version."""
return send_from_directory('beta', path)
@app.route('/gamma/<path:path>')
def gamma(path):
"""Fallback for serving gamma version."""
return send_from_directory('gamma', path)
@app.route('/delta/<path:path>')
def delta(path):
"""Fallback for serving delta version."""
return send_from_directory('delta', path)
@app.route('/pm', methods=['GET', 'OPTIONS', 'POST'])
def pm():
"""Main request handler."""
method = request.form['method']
if method not in globals():
raise InvalidUsage('This method is not yet implemented',

View File

@ -2,7 +2,7 @@
from migrate.versioning import api
from config import SQLALCHEMY_DATABASE_URI
from config import SQLALCHEMY_MIGRATE_REPO
from app import db
from clipperz import db
import os.path
db.create_all()
if not os.path.exists(SQLALCHEMY_MIGRATE_REPO):

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
import imp
from migrate.versioning import api
from app import db
from clipperz import db
from config import SQLALCHEMY_DATABASE_URI
from config import SQLALCHEMY_MIGRATE_REPO
v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO)

View File

@ -0,0 +1,97 @@
from sqlalchemy import *
from migrate import *
from migrate.changeset import schema
pre_meta = MetaData()
post_meta = MetaData()
one_time_password = Table('one_time_password', post_meta,
Column('id', Integer, primary_key=True, nullable=False),
Column('user_id', Integer),
Column('status', String),
Column('reference', String),
Column('key_value', String),
Column('key_checksum', String),
Column('data', Text),
Column('version', String),
Column('creation_date', DateTime),
Column('request_date', DateTime),
Column('usage_date', DateTime),
)
record = Table('record', post_meta,
Column('id', Integer, primary_key=True, nullable=False),
Column('user_id', Integer),
Column('reference', String),
Column('data', Text),
Column('api_version', String),
Column('version', Integer, default=ColumnDefault(0)),
Column('creation_date', DateTime),
Column('update_date', DateTime),
Column('access_date', DateTime),
)
record_version = Table('record_version', post_meta,
Column('id', Integer, primary_key=True, nullable=False),
Column('reference', String),
Column('header', Text),
Column('data', Text),
Column('api_version', String),
Column('version', Integer),
Column('previous_version_key', String),
Column('previous_version_id', Integer),
Column('creation_date', DateTime),
Column('update_date', DateTime),
Column('access_date', DateTime),
Column('record_id', Integer, nullable=False),
)
session = Table('session', post_meta,
Column('id', Integer, primary_key=True, nullable=False),
Column('sessionId', String),
Column('access_date', DateTime),
)
sessions = Table('sessions', post_meta,
Column('key', String(length=250), primary_key=True, nullable=False),
Column('value', LargeBinary, nullable=False),
)
user = Table('user', post_meta,
Column('id', Integer, primary_key=True, nullable=False),
Column('username', String),
Column('srp_s', String(length=128)),
Column('srp_v', String(length=128)),
Column('header', Text),
Column('statistics', Text),
Column('auth_version', String),
Column('version', String),
Column('lock', String),
Column('offline_saved', Boolean, default=ColumnDefault(False)),
Column('update_date', DateTime),
)
def upgrade(migrate_engine):
# Upgrade operations go here. Don't create your own engine; bind
# migrate_engine to your metadata
pre_meta.bind = migrate_engine
post_meta.bind = migrate_engine
post_meta.tables['one_time_password'].create()
post_meta.tables['record'].create()
post_meta.tables['record_version'].create()
post_meta.tables['session'].create()
post_meta.tables['sessions'].create()
post_meta.tables['user'].create()
def downgrade(migrate_engine):
# Operations to reverse the above upgrade go here.
pre_meta.bind = migrate_engine
post_meta.bind = migrate_engine
post_meta.tables['one_time_password'].drop()
post_meta.tables['record'].drop()
post_meta.tables['record_version'].drop()
post_meta.tables['session'].drop()
post_meta.tables['sessions'].drop()
post_meta.tables['user'].drop()

View File

@ -45,6 +45,7 @@ setup(
install_requires=['Flask>=0.10.1',
'Flask-SQLAlchemy>=1.0',
'SQLAlchemy>=0.8.2',
'SQLAlchemy-migrate',
'Flask-Login',
'Flask-KVSession',
],