Feature: Login history logging

Feature: Clear out old/unused clients using Creation Date
Breakage: Change in structure of Clients table and addition of Login History table
This commit is contained in:
Amritanshu 2014-08-30 17:46:43 +05:30
parent 25a82a2027
commit f3b4e95072
6 changed files with 97 additions and 34 deletions

View File

@ -28,7 +28,7 @@ def fixtures(engine):
from brewman.models.messaging import Tag, Thread, Subscriber, thread_tag, Post
from brewman.models.voucher import Attendance, Batch, Fingerprint, Inventory, Journal, Product, SalaryDeduction, Voucher, VoucherType
from brewman.models.master import Product, AttendanceType, CostCenter, Employee, Ledger, LedgerBase, LedgerType, ProductGroup, MenuItem, Recipe, RecipeItem
from brewman.models.auth import Client, Group, Role, User, role_group, user_group
from brewman.models.auth import Client, Group, Role, User, role_group, user_group, LoginHistory
Base.metadata.create_all(engine)

View File

@ -2,9 +2,10 @@ import random
import string
import uuid
from hashlib import md5
from datetime import datetime
from sqlalchemy.schema import ForeignKey, Table
from sqlalchemy import Column, Boolean, Unicode, Integer
from sqlalchemy import Column, Boolean, Unicode, Integer, DateTime, UniqueConstraint
from sqlalchemy.orm import synonym, relationship
from brewman.models.guidtype import GUID
@ -19,17 +20,22 @@ def encrypt(val):
class Client(Base):
__tablename__ = 'auth_clients'
id = Column('ClientID', GUID(), primary_key=True, default=uuid.uuid4)
code = Column('Code', Integer, unique=True, nullable=False)
name = Column('Name', Unicode(255), unique=True, nullable=False)
enabled = Column('Enabled', Boolean, nullable=False)
otp = Column('OTP', Integer)
id = Column('client_id', GUID(), primary_key=True, default=uuid.uuid4)
code = Column('code', Integer, unique=True, nullable=False)
name = Column('name', Unicode(255), unique=True, nullable=False)
enabled = Column('enabled', Boolean, nullable=False)
otp = Column('otp', Integer)
creation_date = Column('creation_date', DateTime(timezone=True), nullable=False)
def __init__(self, code=None, name=None, enabled=False, otp=None):
login_history = relationship('LoginHistory', backref='client')
def __init__(self, code=None, name=None, enabled=False, otp=None, creation_date=None, id=None):
self.code = code
self.name = name
self.enabled = enabled
self.otp = otp
self.creation_date = datetime.utcnow() if creation_date is None else creation_date
self.id = id
@classmethod
def by_id(cls, id):
@ -85,6 +91,7 @@ class User(Base):
locked_out = Column('LockedOut', Boolean)
groups = relationship("Group", secondary=user_group)
login_history = relationship('LoginHistory', backref='user')
def _get_password(self):
return self._password
@ -146,6 +153,21 @@ class User(Base):
return query.order_by(cls.name)
class LoginHistory(Base):
__tablename__ = 'auth_login_history'
__table_args__ = (UniqueConstraint('user_id', 'client_id', 'date'), )
id = Column('login_history_id', GUID(), primary_key=True, default=uuid.uuid4)
user_id = Column('user_id', GUID(), ForeignKey('auth_users.UserID'), nullable=False)
client_id = Column('client_id', GUID(), ForeignKey('auth_clients.client_id'), nullable=False)
date = Column('date', DateTime(timezone=True), nullable=False)
def __init__(self, user_id=None, client_id=None, date=None, id=None):
self.user_id = user_id
self.client_id = client_id
self.date = datetime.utcnow() if date is None else date
self.id = id
class Group(Base):
__tablename__ = 'auth_groups'

View File

@ -2,8 +2,9 @@ from datetime import datetime
import uuid
from sqlalchemy.dialects.postgresql import BYTEA
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy import Column, Integer, Boolean, Unicode, DateTime, Numeric, ForeignKey, UniqueConstraint
from sqlalchemy import Column, Integer, Boolean, Unicode, DateTime, Numeric, ForeignKey, UniqueConstraint, Index
from sqlalchemy.orm import relationship, synonym, backref
from brewman.models.guidtype import GUID
@ -53,7 +54,7 @@ class Voucher(Base):
__tablename__ = 'vouchers'
id = Column('VoucherID', GUID(), primary_key=True, default=uuid.uuid4)
date = Column('Date', DateTime, nullable=False)
date = Column('Date', DateTime, nullable=False, index=True)
reconcile_date = Column('ReconcileDate', DateTime, nullable=False)
is_reconciled = Column('IsReconciled', Boolean, nullable=False)
narration = Column('Narration', Unicode(1000), nullable=False)
@ -72,6 +73,8 @@ class Voucher(Base):
salary_deductions = relationship('SalaryDeduction', backref='voucher', cascade="delete, delete-orphan",
cascade_backrefs=False)
def _get_type(self):
return self._type
# for item in VoucherType.list():
@ -123,7 +126,7 @@ class Journal(Base):
id = Column('JournalID', GUID(), primary_key=True, default=uuid.uuid4)
debit = Column('Debit', Integer)
amount = Column('Amount', Numeric)
voucher_id = Column('VoucherID', GUID(), ForeignKey('vouchers.VoucherID'), nullable=False)
voucher_id = Column('VoucherID', GUID(), ForeignKey('vouchers.VoucherID'), nullable=False, index=True)
ledger_id = Column('LedgerID', GUID(), ForeignKey('ledgers.LedgerID'), nullable=False)
cost_center_id = Column('CostCenterID', GUID(), ForeignKey('cost_centers.CostCenterID'), nullable=False)
@ -190,7 +193,7 @@ class Inventory(Base):
__tablename__ = 'inventories'
__table_args__ = (UniqueConstraint('VoucherID', 'BatchID'), )
id = Column('InventoryID', GUID(), primary_key=True, default=uuid.uuid4)
voucher_id = Column('VoucherID', GUID(), ForeignKey('vouchers.VoucherID'), nullable=False)
voucher_id = Column('VoucherID', GUID(), ForeignKey('vouchers.VoucherID'), nullable=False, index=True)
product_id = Column('ProductID', GUID(), ForeignKey('products.ProductID'), nullable=False)
batch_id = Column('BatchID', GUID(), ForeignKey('batches.BatchID'), nullable=False)
quantity = Column('Quantity', Numeric)

View File

@ -6,6 +6,7 @@
<th>Name</th>
<th>Enabled</th>
<th>OTP</th>
<th>Created</th>
</tr>
</thead>
<tbody>
@ -14,6 +15,7 @@
<td><a href="{{item.Url}}">{{item.Name}}</a></td>
<td>{{item.Enabled}}</td>
<td>{{item.OTP}}</td>
<td>{{item.CreationDate | localTime}}</td>
</tr>
</tbody>
</table>

View File

@ -1,12 +1,12 @@
import uuid
import pkg_resources
from pyramid.response import Response, FileResponse
from pyramid.view import view_config
import transaction
from brewman.models import DBSession
from brewman.models.auth import Client
from brewman.models.validation_exception import TryCatchFunction
@ -56,6 +56,7 @@ def show_list(request):
for item in list:
clients.append(
{'ClientID': item.id, 'Code': item.code, 'Name': item.name, 'Enabled': item.enabled, 'OTP': item.otp,
'CreationDate': item.creation_date.strftime('%d-%b-%Y %H:%M'),
'Url': request.route_url('client_id', id=item.id)})
return clients

View File

@ -1,11 +1,18 @@
from datetime import datetime, timedelta
from pyramid.httpexceptions import HTTPFound
from pyramid.response import Response
from pyramid.security import remember, forget
from pyramid.security import remember, forget
from pyramid.view import view_config
from sqlalchemy import and_, or_
import transaction
from brewman import groupfinder
from brewman.models.auth import User, Client
from brewman.models import DBSession
from brewman.models.auth import User, Client, LoginHistory
from brewman.models.validation_exception import TryCatchFunction
@view_config(route_name='logout')
def logout(request):
@ -15,32 +22,35 @@ def logout(request):
@view_config(request_method='POST', route_name='api_login', renderer='json')
@TryCatchFunction
def login(request):
username = request.json_body.get('username', None)
password = request.json_body.get('password', None)
found, user = User.auth(username, password)
client = request.cookies.get('ClientID', None)
client = Client.by_code(request.cookies.get('ClientID', None))
otp = request.json_body.get('otp', None)
client_name = request.json_body.get('ClientName', None)
allowed, response = check_client(client, otp, client_name, user if found else None)
allowed, client, response = check_client(client, otp, client_name, user if found else None)
if found and allowed:
headers = remember(request, str(user.id))
request.response.headers = headers
if allowed and isinstance(response, Client):
request.response.set_cookie('ClientID', value=str(response.code), max_age=10 * 365 * 24 * 60 * 60)
request.response.set_cookie('ClientID', value=str(client.code), max_age=365 * 24 * 60 * 60)
record_login(user.id, client)
transaction.commit()
return request.response
elif not found:
response = Response("Login failed")
response.status_int = 403
transaction.commit()
return response
else:
transaction.commit()
return response
def check_client(client, otp, client_name, user):
client = None if client is None else Client.by_code(client)
outside_login_allowed = False if user is None else True if 'Clients' in groupfinder(user.id, None) else False
if len(Client.enabled_list()) == 0 and outside_login_allowed:
@ -48,41 +58,66 @@ def check_client(client, otp, client_name, user):
client.otp = None
client.name = 'Created on login by ' + user.name
client.enabled = True
transaction.commit()
return True, client
return True, client, None
if client is None:
if outside_login_allowed:
client = Client.create()
transaction.commit()
return True, client
return True, client, None
else:
client = Client.create()
response = Response("Unknown Client")
response.status_int = 403
response.set_cookie('ClientID', value=str(client.code), max_age=10 * 365 * 24 * 60 * 60)
transaction.commit()
return False, response
return False, None, response
if client.enabled or outside_login_allowed:
return True, None
return True, client, None
if client.otp is None:
if not client.otp is None:
response = Response("Client is Forbidden")
response.status_int = 403
return False, response
return False, None, response
if otp is None:
response = Response("OTP not supplied")
response.status_int = 403
return False, response
return False, None, response
elif client.otp != int(otp):
response = Response("OTP is wrong")
response.status_int = 403
return False, response
return False, None, response
else:
client.otp = None
client.enabled = True
client.name = client_name
transaction.commit()
return True, None
return True, client, None
def record_login(user_id, client):
history = LoginHistory(user_id)
history.client = client
DBSession.add(history)
recent_logins = DBSession.query(LoginHistory.client_id.distinct()) \
.filter(LoginHistory.date > datetime.utcnow() - timedelta(days=90)).subquery()
deletable_clients = DBSession.query(Client.id) \
.filter(Client.creation_date < datetime.utcnow() - timedelta(days=3)) \
.filter(Client.enabled == False).subquery()
LoginHistory.__table__.delete(
and_(
~LoginHistory.client_id.in_(recent_logins),
LoginHistory.client_id.in_(deletable_clients)
)
).execute()
Client.__table__.delete(
and_(
Client.creation_date < datetime.utcnow() - timedelta(days=3),
Client.enabled == False,
~Client.id.in_(recent_logins)
)
).execute()