|
|
|
@ -1,18 +1,23 @@
@@ -1,18 +1,23 @@
|
|
|
|
|
''' User Account and Roles Models ''' |
|
|
|
|
|
|
|
|
|
import enum |
|
|
|
|
import uuid |
|
|
|
|
|
|
|
|
|
from datetime import datetime |
|
|
|
|
from datetime import datetime, timedelta |
|
|
|
|
from passlib.context import CryptContext |
|
|
|
|
from sqlalchemy import ( |
|
|
|
|
Column, |
|
|
|
|
Date, |
|
|
|
|
DateTime, |
|
|
|
|
Enum, |
|
|
|
|
ForeignKey, |
|
|
|
|
Integer, |
|
|
|
|
Text, |
|
|
|
|
Unicode |
|
|
|
|
) |
|
|
|
|
from sqlalchemy.orm import relationship |
|
|
|
|
|
|
|
|
|
from .meta import Base |
|
|
|
|
from .meta import Base, JsonEncoder |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#: create a crypt context for password hashes |
|
|
|
@ -21,6 +26,8 @@ from .meta import Base
@@ -21,6 +26,8 @@ from .meta import Base
|
|
|
|
|
passlib_context = CryptContext() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# non-database models |
|
|
|
|
|
|
|
|
|
class Role(enum.Enum): |
|
|
|
|
''' roles of user accounts ''' |
|
|
|
|
|
|
|
|
@ -52,6 +59,21 @@ class Role(enum.Enum):
@@ -52,6 +59,21 @@ class Role(enum.Enum):
|
|
|
|
|
return self.value.capitalize() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TokenSubject(enum.Enum): |
|
|
|
|
''' Token Subjects for changing user accounts ''' |
|
|
|
|
|
|
|
|
|
#: validate email address of freshly registered user |
|
|
|
|
USER_REGISTRATION = 'user_registration' |
|
|
|
|
|
|
|
|
|
#: validate email change of active user |
|
|
|
|
CHANGE_EMAIL = 'change_email' |
|
|
|
|
|
|
|
|
|
#: reset a forgotten password |
|
|
|
|
RESET_PASSWORD = 'reset_password' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# database driven models |
|
|
|
|
|
|
|
|
|
class User(Base): |
|
|
|
|
''' A user of the application ''' |
|
|
|
|
|
|
|
|
@ -71,6 +93,13 @@ class User(Base):
@@ -71,6 +93,13 @@ class User(Base):
|
|
|
|
|
email = Column(Text, nullable=False, unique=True) |
|
|
|
|
date_created = Column(Date, nullable=False, default=datetime.utcnow) |
|
|
|
|
|
|
|
|
|
#: tokens for new user registration, email change and forgotten passwords |
|
|
|
|
tokens = relationship( |
|
|
|
|
'Token', |
|
|
|
|
back_populates='owner', |
|
|
|
|
cascade="all, delete-orphan" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
@property |
|
|
|
|
def principal(self): |
|
|
|
|
''' returns the principal identifier for the user ''' |
|
|
|
@ -119,6 +148,76 @@ class User(Base):
@@ -119,6 +148,76 @@ class User(Base):
|
|
|
|
|
# password match, return True |
|
|
|
|
return True |
|
|
|
|
|
|
|
|
|
def issue_token(self, request, subject, payload=None): |
|
|
|
|
''' issues a token for mail change, password reset or user verification |
|
|
|
|
|
|
|
|
|
:param request: |
|
|
|
|
the current request object |
|
|
|
|
:type request: |
|
|
|
|
pyramid.request.Request |
|
|
|
|
:param subject: |
|
|
|
|
what the token is used for |
|
|
|
|
:type subject: |
|
|
|
|
ordr2.models.account.TokenSubject |
|
|
|
|
:param **payload: |
|
|
|
|
etra data to store with the token, must be JSON serializable |
|
|
|
|
:rtype: |
|
|
|
|
(str) unique hash to access the token |
|
|
|
|
''' |
|
|
|
|
token = Token.issue(request, self, subject, payload) |
|
|
|
|
return token.hash |
|
|
|
|
|
|
|
|
|
def __str__(self): |
|
|
|
|
''' string representation ''' |
|
|
|
|
return str(self.username) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Token(Base): |
|
|
|
|
''' Tokens for mail change, account verification and password reset ''' |
|
|
|
|
|
|
|
|
|
__tablename__ = 'tokens' |
|
|
|
|
hash = Column(Unicode, primary_key=True) |
|
|
|
|
subject = Column(Enum(TokenSubject), nullable=False) |
|
|
|
|
expires = Column(DateTime, nullable=False) |
|
|
|
|
payload = Column(JsonEncoder, nullable=False) |
|
|
|
|
owner_id = Column(Integer, ForeignKey('users.id')) |
|
|
|
|
owner = relationship('User', back_populates='tokens') |
|
|
|
|
|
|
|
|
|
@classmethod |
|
|
|
|
def issue(cls, request, owner, subject, payload=None): |
|
|
|
|
''' issues a token for mail change, password reset or user verification |
|
|
|
|
|
|
|
|
|
if the expiry keys for the token is not set in the app configuration, |
|
|
|
|
the token will expire in five minutes. |
|
|
|
|
|
|
|
|
|
to set the expiry time in the conig use `token_expiry.` prefix followed |
|
|
|
|
by the value of the token subject and a time in minutes. For example, |
|
|
|
|
to give an active user two hours time to verify an email address change |
|
|
|
|
use `token_expiry.change_email = 120` |
|
|
|
|
|
|
|
|
|
:param request: |
|
|
|
|
the current request object |
|
|
|
|
:type request: |
|
|
|
|
pyramid.request.Request |
|
|
|
|
:param subject: |
|
|
|
|
what the token is used for |
|
|
|
|
:type subject: |
|
|
|
|
ordr2.models.account.TokenSubject |
|
|
|
|
:param owner: |
|
|
|
|
account the token is issued for |
|
|
|
|
:type subject: |
|
|
|
|
ordr2.models.account.User |
|
|
|
|
:param **payload: |
|
|
|
|
etra data to store with the token, must be JSON serializable |
|
|
|
|
:rtype: |
|
|
|
|
ordr2.models.account.Token |
|
|
|
|
''' |
|
|
|
|
settings_key = 'token_expiry.' + subject.value |
|
|
|
|
minutes = request.registry.settings.get(settings_key, 5) |
|
|
|
|
expires = datetime.utcnow() + timedelta(minutes=int(minutes)) |
|
|
|
|
return cls( |
|
|
|
|
hash=uuid.uuid4().hex, |
|
|
|
|
subject=subject, |
|
|
|
|
payload=payload, |
|
|
|
|
owner=owner, |
|
|
|
|
expires=expires) |
|
|
|
|