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.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)
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue