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:
parent
25a82a2027
commit
f3b4e95072
|
@ -28,7 +28,7 @@ def fixtures(engine):
|
||||||
from brewman.models.messaging import Tag, Thread, Subscriber, thread_tag, Post
|
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.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.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)
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,10 @@ import random
|
||||||
import string
|
import string
|
||||||
import uuid
|
import uuid
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy.schema import ForeignKey, Table
|
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 sqlalchemy.orm import synonym, relationship
|
||||||
|
|
||||||
from brewman.models.guidtype import GUID
|
from brewman.models.guidtype import GUID
|
||||||
|
@ -19,17 +20,22 @@ def encrypt(val):
|
||||||
class Client(Base):
|
class Client(Base):
|
||||||
__tablename__ = 'auth_clients'
|
__tablename__ = 'auth_clients'
|
||||||
|
|
||||||
id = Column('ClientID', GUID(), primary_key=True, default=uuid.uuid4)
|
id = Column('client_id', GUID(), primary_key=True, default=uuid.uuid4)
|
||||||
code = Column('Code', Integer, unique=True, nullable=False)
|
code = Column('code', Integer, unique=True, nullable=False)
|
||||||
name = Column('Name', Unicode(255), unique=True, nullable=False)
|
name = Column('name', Unicode(255), unique=True, nullable=False)
|
||||||
enabled = Column('Enabled', Boolean, nullable=False)
|
enabled = Column('enabled', Boolean, nullable=False)
|
||||||
otp = Column('OTP', Integer)
|
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.code = code
|
||||||
self.name = name
|
self.name = name
|
||||||
self.enabled = enabled
|
self.enabled = enabled
|
||||||
self.otp = otp
|
self.otp = otp
|
||||||
|
self.creation_date = datetime.utcnow() if creation_date is None else creation_date
|
||||||
|
self.id = id
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def by_id(cls, id):
|
def by_id(cls, id):
|
||||||
|
@ -85,6 +91,7 @@ class User(Base):
|
||||||
locked_out = Column('LockedOut', Boolean)
|
locked_out = Column('LockedOut', Boolean)
|
||||||
|
|
||||||
groups = relationship("Group", secondary=user_group)
|
groups = relationship("Group", secondary=user_group)
|
||||||
|
login_history = relationship('LoginHistory', backref='user')
|
||||||
|
|
||||||
def _get_password(self):
|
def _get_password(self):
|
||||||
return self._password
|
return self._password
|
||||||
|
@ -146,6 +153,21 @@ class User(Base):
|
||||||
return query.order_by(cls.name)
|
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):
|
class Group(Base):
|
||||||
__tablename__ = 'auth_groups'
|
__tablename__ = 'auth_groups'
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,9 @@ from datetime import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from sqlalchemy.dialects.postgresql import BYTEA
|
from sqlalchemy.dialects.postgresql import BYTEA
|
||||||
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
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 sqlalchemy.orm import relationship, synonym, backref
|
||||||
|
|
||||||
from brewman.models.guidtype import GUID
|
from brewman.models.guidtype import GUID
|
||||||
|
@ -53,7 +54,7 @@ class Voucher(Base):
|
||||||
__tablename__ = 'vouchers'
|
__tablename__ = 'vouchers'
|
||||||
|
|
||||||
id = Column('VoucherID', GUID(), primary_key=True, default=uuid.uuid4)
|
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)
|
reconcile_date = Column('ReconcileDate', DateTime, nullable=False)
|
||||||
is_reconciled = Column('IsReconciled', Boolean, nullable=False)
|
is_reconciled = Column('IsReconciled', Boolean, nullable=False)
|
||||||
narration = Column('Narration', Unicode(1000), 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",
|
salary_deductions = relationship('SalaryDeduction', backref='voucher', cascade="delete, delete-orphan",
|
||||||
cascade_backrefs=False)
|
cascade_backrefs=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _get_type(self):
|
def _get_type(self):
|
||||||
return self._type
|
return self._type
|
||||||
# for item in VoucherType.list():
|
# for item in VoucherType.list():
|
||||||
|
@ -123,7 +126,7 @@ class Journal(Base):
|
||||||
id = Column('JournalID', GUID(), primary_key=True, default=uuid.uuid4)
|
id = Column('JournalID', GUID(), primary_key=True, default=uuid.uuid4)
|
||||||
debit = Column('Debit', Integer)
|
debit = Column('Debit', Integer)
|
||||||
amount = Column('Amount', Numeric)
|
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)
|
ledger_id = Column('LedgerID', GUID(), ForeignKey('ledgers.LedgerID'), nullable=False)
|
||||||
cost_center_id = Column('CostCenterID', GUID(), ForeignKey('cost_centers.CostCenterID'), nullable=False)
|
cost_center_id = Column('CostCenterID', GUID(), ForeignKey('cost_centers.CostCenterID'), nullable=False)
|
||||||
|
|
||||||
|
@ -190,7 +193,7 @@ class Inventory(Base):
|
||||||
__tablename__ = 'inventories'
|
__tablename__ = 'inventories'
|
||||||
__table_args__ = (UniqueConstraint('VoucherID', 'BatchID'), )
|
__table_args__ = (UniqueConstraint('VoucherID', 'BatchID'), )
|
||||||
id = Column('InventoryID', GUID(), primary_key=True, default=uuid.uuid4)
|
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)
|
product_id = Column('ProductID', GUID(), ForeignKey('products.ProductID'), nullable=False)
|
||||||
batch_id = Column('BatchID', GUID(), ForeignKey('batches.BatchID'), nullable=False)
|
batch_id = Column('BatchID', GUID(), ForeignKey('batches.BatchID'), nullable=False)
|
||||||
quantity = Column('Quantity', Numeric)
|
quantity = Column('Quantity', Numeric)
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Enabled</th>
|
<th>Enabled</th>
|
||||||
<th>OTP</th>
|
<th>OTP</th>
|
||||||
|
<th>Created</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
<td><a href="{{item.Url}}">{{item.Name}}</a></td>
|
<td><a href="{{item.Url}}">{{item.Name}}</a></td>
|
||||||
<td>{{item.Enabled}}</td>
|
<td>{{item.Enabled}}</td>
|
||||||
<td>{{item.OTP}}</td>
|
<td>{{item.OTP}}</td>
|
||||||
|
<td>{{item.CreationDate | localTime}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
from pyramid.response import Response, FileResponse
|
from pyramid.response import Response, FileResponse
|
||||||
|
|
||||||
from pyramid.view import view_config
|
from pyramid.view import view_config
|
||||||
import transaction
|
import transaction
|
||||||
|
|
||||||
from brewman.models import DBSession
|
from brewman.models import DBSession
|
||||||
from brewman.models.auth import Client
|
from brewman.models.auth import Client
|
||||||
|
|
||||||
from brewman.models.validation_exception import TryCatchFunction
|
from brewman.models.validation_exception import TryCatchFunction
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,6 +56,7 @@ def show_list(request):
|
||||||
for item in list:
|
for item in list:
|
||||||
clients.append(
|
clients.append(
|
||||||
{'ClientID': item.id, 'Code': item.code, 'Name': item.name, 'Enabled': item.enabled, 'OTP': item.otp,
|
{'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)})
|
'Url': request.route_url('client_id', id=item.id)})
|
||||||
return clients
|
return clients
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from pyramid.httpexceptions import HTTPFound
|
from pyramid.httpexceptions import HTTPFound
|
||||||
from pyramid.response import Response
|
from pyramid.response import Response
|
||||||
from pyramid.security import remember, forget
|
|
||||||
|
|
||||||
|
from pyramid.security import remember, forget
|
||||||
from pyramid.view import view_config
|
from pyramid.view import view_config
|
||||||
|
from sqlalchemy import and_, or_
|
||||||
import transaction
|
import transaction
|
||||||
|
|
||||||
from brewman import groupfinder
|
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')
|
@view_config(route_name='logout')
|
||||||
def logout(request):
|
def logout(request):
|
||||||
|
@ -15,32 +22,35 @@ def logout(request):
|
||||||
|
|
||||||
|
|
||||||
@view_config(request_method='POST', route_name='api_login', renderer='json')
|
@view_config(request_method='POST', route_name='api_login', renderer='json')
|
||||||
|
@TryCatchFunction
|
||||||
def login(request):
|
def login(request):
|
||||||
username = request.json_body.get('username', None)
|
username = request.json_body.get('username', None)
|
||||||
password = request.json_body.get('password', None)
|
password = request.json_body.get('password', None)
|
||||||
found, user = User.auth(username, password)
|
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)
|
otp = request.json_body.get('otp', None)
|
||||||
client_name = request.json_body.get('ClientName', 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:
|
if found and allowed:
|
||||||
headers = remember(request, str(user.id))
|
headers = remember(request, str(user.id))
|
||||||
request.response.headers = headers
|
request.response.headers = headers
|
||||||
if allowed and isinstance(response, Client):
|
request.response.set_cookie('ClientID', value=str(client.code), max_age=365 * 24 * 60 * 60)
|
||||||
request.response.set_cookie('ClientID', value=str(response.code), max_age=10 * 365 * 24 * 60 * 60)
|
record_login(user.id, client)
|
||||||
|
transaction.commit()
|
||||||
return request.response
|
return request.response
|
||||||
elif not found:
|
elif not found:
|
||||||
response = Response("Login failed")
|
response = Response("Login failed")
|
||||||
response.status_int = 403
|
response.status_int = 403
|
||||||
|
transaction.commit()
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
|
transaction.commit()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def check_client(client, otp, client_name, user):
|
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
|
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:
|
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.otp = None
|
||||||
client.name = 'Created on login by ' + user.name
|
client.name = 'Created on login by ' + user.name
|
||||||
client.enabled = True
|
client.enabled = True
|
||||||
transaction.commit()
|
return True, client, None
|
||||||
return True, client
|
|
||||||
|
|
||||||
if client is None:
|
if client is None:
|
||||||
if outside_login_allowed:
|
if outside_login_allowed:
|
||||||
client = Client.create()
|
client = Client.create()
|
||||||
transaction.commit()
|
return True, client, None
|
||||||
return True, client
|
|
||||||
else:
|
else:
|
||||||
client = Client.create()
|
client = Client.create()
|
||||||
response = Response("Unknown Client")
|
response = Response("Unknown Client")
|
||||||
response.status_int = 403
|
response.status_int = 403
|
||||||
response.set_cookie('ClientID', value=str(client.code), max_age=10 * 365 * 24 * 60 * 60)
|
response.set_cookie('ClientID', value=str(client.code), max_age=10 * 365 * 24 * 60 * 60)
|
||||||
transaction.commit()
|
return False, None, response
|
||||||
return False, response
|
|
||||||
|
|
||||||
if client.enabled or outside_login_allowed:
|
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 = Response("Client is Forbidden")
|
||||||
response.status_int = 403
|
response.status_int = 403
|
||||||
return False, response
|
return False, None, response
|
||||||
|
|
||||||
if otp is None:
|
if otp is None:
|
||||||
response = Response("OTP not supplied")
|
response = Response("OTP not supplied")
|
||||||
response.status_int = 403
|
response.status_int = 403
|
||||||
return False, response
|
return False, None, response
|
||||||
elif client.otp != int(otp):
|
elif client.otp != int(otp):
|
||||||
response = Response("OTP is wrong")
|
response = Response("OTP is wrong")
|
||||||
response.status_int = 403
|
response.status_int = 403
|
||||||
return False, response
|
return False, None, response
|
||||||
else:
|
else:
|
||||||
client.otp = None
|
client.otp = None
|
||||||
client.enabled = True
|
client.enabled = True
|
||||||
client.name = client_name
|
client.name = client_name
|
||||||
transaction.commit()
|
return True, client, None
|
||||||
return True, 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()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue