# encoding: utf-8
# Credential, DRMDeviceIdentifier, DelegatedPatronIdentifier
import datetime
import uuid
import sqlalchemy
from sqlalchemy import (
Column,
DateTime,
ForeignKey,
Index,
Integer,
String,
UniqueConstraint,
)
from sqlalchemy.orm import backref, relationship
from sqlalchemy.orm.session import Session
from sqlalchemy.sql.expression import and_
from . import Base, get_one, get_one_or_create
from ..util import is_session
from ..util.datetime_helpers import utc_now
[docs]class Credential(Base):
"""A place to store credentials for external services."""
__tablename__ = 'credentials'
id = Column(Integer, primary_key=True)
data_source_id = Column(Integer, ForeignKey('datasources.id'), index=True)
patron_id = Column(Integer, ForeignKey('patrons.id'), index=True)
collection_id = Column(Integer, ForeignKey('collections.id'), index=True)
type = Column(String(255), index=True)
credential = Column(String)
expires = Column(DateTime(timezone=True), index=True)
# One Credential can have many associated DRMDeviceIdentifiers.
drm_device_identifiers = relationship(
"DRMDeviceIdentifier", backref=backref("credential", lazy='joined')
)
__table_args__ = (
# Unique indexes to prevent the creation of redundant credentials.
# If both patron_id and collection_id are null, then (data_source_id,
# type, credential) must be unique.
Index(
"ix_credentials_data_source_id_type_token",
data_source_id, type, credential, unique=True,
postgresql_where=and_(patron_id==None, collection_id==None)
),
# If patron_id is null but collection_id is not, then
# (data_source, type, collection_id) must be unique.
Index(
"ix_credentials_data_source_id_type_collection_id",
data_source_id, type, collection_id,
unique=True, postgresql_where=(patron_id==None)
),
# If collection_id is null but patron_id is not, then
# (data_source, type, patron_id) must be unique.
# (At the moment this never happens.)
Index(
"ix_credentials_data_source_id_type_patron_id",
data_source_id, type, patron_id,
unique=True, postgresql_where=(collection_id==None)
),
# If neither collection_id nor patron_id is null, then
# (data_source, type, patron_id, collection_id)
# must be unique.
Index(
"ix_credentials_data_source_id_type_patron_id_collection_id",
data_source_id, type, patron_id, collection_id,
unique=True,
),
)
# A meaningless identifier used to identify this patron (and no other)
# to a remote service.
IDENTIFIER_TO_REMOTE_SERVICE = "Identifier Sent To Remote Service"
# An identifier used by a remote service to identify this patron.
IDENTIFIER_FROM_REMOTE_SERVICE = "Identifier Received From Remote Service"
@classmethod
def _filter_invalid_credential(cls, credential, allow_persistent_token):
"""Filter out invalid credentials based on their expiration time and persistence.
:param credential: Credential object
:type credential: Credential
:param allow_persistent_token: Boolean value indicating whether persistent tokens are allowed
:type allow_persistent_token: bool
"""
if not credential:
# No matching token.
return None
if not credential.expires:
if allow_persistent_token:
return credential
else:
# It's an error that this token never expires. It's invalid.
return None
elif credential.expires > utc_now():
return credential
else:
# Token has expired.
return None
[docs] @classmethod
def lookup(cls, _db, data_source, token_type, patron, refresher_method,
allow_persistent_token=False, allow_empty_token=False,
collection=None, force_refresh=False):
from .datasource import DataSource
if isinstance(data_source, str):
data_source = DataSource.lookup(_db, data_source)
credential, is_new = get_one_or_create(
_db, Credential, data_source=data_source, type=token_type, patron=patron, collection=collection)
if (is_new
or force_refresh
or (not credential.expires and not allow_persistent_token)
or (not credential.credential and not allow_empty_token)
or (credential.expires
and credential.expires <= utc_now())):
if refresher_method:
refresher_method(credential)
return credential
[docs] @classmethod
def lookup_by_token(
cls,
_db,
data_source,
token_type,
token,
allow_persistent_token=False
):
"""Look up a unique token.
Lookup will fail on expired tokens. Unless persistent tokens
are specifically allowed, lookup will fail on persistent tokens.
"""
credential = get_one(
_db, Credential, data_source=data_source, type=token_type,
credential=token)
return cls._filter_invalid_credential(credential, allow_persistent_token)
[docs] @classmethod
def lookup_by_patron(
cls,
_db,
data_source_name,
token_type,
patron,
allow_persistent_token=False,
auto_create_datasource=True
):
"""Look up a unique token.
Lookup will fail on expired tokens. Unless persistent tokens
are specifically allowed, lookup will fail on persistent tokens.
:param _db: Database session
:type _db: sqlalchemy.orm.session.Session
:param data_source_name: Name of the data source
:type data_source_name: str
:param token_type: Token type
:type token_type: str
:param patron: Patron object
:type patron: core.model.patron.Patron
:param allow_persistent_token: Boolean value indicating whether persistent tokens are allowed or not
:type allow_persistent_token: bool
:param auto_create_datasource: Boolean value indicating whether
a data source should be created in the case it doesn't
:type auto_create_datasource: bool
"""
from .patron import Patron
if not is_session(_db):
raise ValueError('"_db" argument must be a valid SQLAlchemy session')
if not isinstance(data_source_name, str) or not data_source_name:
raise ValueError('"data_source_name" argument must be a non-empty string')
if not isinstance(token_type, str) or not token_type:
raise ValueError('"token_type" argument must be a non-empty string')
if not isinstance(patron, Patron):
raise ValueError('"patron" argument must be an instance of Patron class')
if not isinstance(allow_persistent_token, bool):
raise ValueError('"allow_persistent_token" argument must be boolean')
if not isinstance(auto_create_datasource, bool):
raise ValueError('"auto_create_datasource" argument must be boolean')
from .datasource import DataSource
data_source = DataSource.lookup(
_db,
data_source_name,
autocreate=auto_create_datasource
)
credential = get_one(
_db,
Credential,
data_source=data_source,
type=token_type,
patron=patron
)
return cls._filter_invalid_credential(credential, allow_persistent_token)
[docs] @classmethod
def lookup_and_expire_temporary_token(cls, _db, data_source, type, token):
"""Look up a temporary token and expire it immediately."""
credential = cls.lookup_by_token(_db, data_source, type, token)
if not credential:
return None
credential.expires = utc_now() - datetime.timedelta(
seconds=5)
return credential
[docs] @classmethod
def temporary_token_create(
cls,
_db,
data_source,
token_type,
patron,
duration,
value=None
):
"""Create a temporary token for the given data_source/type/patron.
The token will be good for the specified `duration`.
"""
expires = utc_now() + duration
token_string = value or str(uuid.uuid1())
credential, is_new = get_one_or_create(
_db, Credential, data_source=data_source, type=token_type, patron=patron)
# If there was already a token of this type for this patron,
# the new one overwrites the old one.
credential.credential=token_string
credential.expires=expires
return credential, is_new
[docs] @classmethod
def persistent_token_create(self, _db, data_source, type, patron, token_string=None):
"""Create or retrieve a persistent token for the given
data_source/type/patron.
"""
if token_string is None:
token_string = str(uuid.uuid1())
credential, is_new = get_one_or_create(
_db, Credential, data_source=data_source, type=type, patron=patron,
create_method_kwargs=dict(credential=token_string)
)
credential.expires=None
return credential, is_new
# A Credential may have many associated DRMDeviceIdentifiers.
[docs] def register_drm_device_identifier(self, device_identifier):
_db = Session.object_session(self)
return get_one_or_create(
_db, DRMDeviceIdentifier,
credential=self,
device_identifier=device_identifier
)
[docs] def deregister_drm_device_identifier(self, device_identifier):
_db = Session.object_session(self)
device_id_obj = get_one(
_db, DRMDeviceIdentifier,
credential=self,
device_identifier=device_identifier
)
if device_id_obj:
_db.delete(device_id_obj)
def __repr__(self):
return \
'<Credential(' \
'data_source_id={0}, ' \
'patron_id={1}, ' \
'collection_id={2}, ' \
'type={3}, ' \
'credential={4}, ' \
'expires={5}>)'.format(
self.data_source_id,
self.patron_id,
self.collection_id,
self.type,
self.credential,
self.expires
)
[docs]class DRMDeviceIdentifier(Base):
"""A device identifier for a particular DRM scheme.
Associated with a Credential, most commonly a patron's "Identifier
for Adobe account ID purposes" Credential.
"""
__tablename__ = 'drmdeviceidentifiers'
id = Column(Integer, primary_key=True)
credential_id = Column(Integer, ForeignKey('credentials.id'), index=True)
device_identifier = Column(String(255), index=True)
[docs]class DelegatedPatronIdentifier(Base):
"""This library is in charge of coming up with, and storing,
identifiers associated with the patrons of some other library.
e.g. NYPL provides Adobe IDs for patrons of all libraries that use
the SimplyE app.
Those identifiers are stored here.
"""
ADOBE_ACCOUNT_ID = 'Adobe Account ID'
__tablename__ = 'delegatedpatronidentifiers'
id = Column(Integer, primary_key=True)
type = Column(String(255), index=True)
library_uri = Column(String(255), index=True)
# This is the ID the foreign library gives us when referring to
# this patron.
patron_identifier = Column(String(255), index=True)
# This is the identifier we made up for the patron. This is what the
# foreign library is trying to look up.
delegated_identifier = Column(String)
__table_args__ = (
UniqueConstraint('type', 'library_uri', 'patron_identifier'),
)
[docs] @classmethod
def get_one_or_create(
cls, _db, library_uri, patron_identifier, identifier_type,
create_function
):
"""Look up the delegated identifier for the given patron. If there is
none, create one.
:param library_uri: A URI identifying the patron's library.
:param patron_identifier: An identifier used by that library to
distinguish between this patron and others. This should be
an identifier created solely for the purpose of identifying the
patron with _this_ library, and not (e.g.) the patron's barcode.
:param identifier_type: The type of the delegated identifier
to look up. (probably ADOBE_ACCOUNT_ID)
:param create_function: If this patron does not have a
DelegatedPatronIdentifier, one will be created, and this
function will be called to determine the value of
DelegatedPatronIdentifier.delegated_identifier.
:return: A 2-tuple (DelegatedPatronIdentifier, is_new)
"""
identifier, is_new = get_one_or_create(
_db, DelegatedPatronIdentifier, library_uri=library_uri,
patron_identifier=patron_identifier, type=identifier_type
)
if is_new:
identifier.delegated_identifier = create_function()
return identifier, is_new